One of our first beta testers — a product manager who switched to us from Cursorful — wrote this after his first recording:
"Pause and restart are super handy, that's really nice. Cursorful doesn't have this."
I didn't even think of it as a feature. I built it almost as an afterthought, it seemed obvious. Turns out it solved a friction point I had underestimated.
That's when you realize you're not building what you thought you were building.
Five lessons from building a Chrome screen recorder — below.
Why bother at all
If you're a designer or developer at a big company, Loom is paid for by the company, everyone uses it. No problem there.
But there's another situation: a side project, the first demo for a client, something for Twitter or Dribbble. When you want to show what you built — polished, with transitions, with zoom — not just a flat gray screen capture.
Turns out there aren't many options:
- QuickTime / built-in recorder — free, but no zoom API, no background compositing, no post-processing at all. Just a raw video file.
- Screen Studio — a great tool, with well-thought-out zoom and backgrounds. But macOS only, and paid. If you're on Windows or Linux, you're out of luck.
- Open-source alternatives — usually mean: clone the repo, install dependencies, figure out the config. Not for someone who wants to make a quick demo between tasks.
- Loom — requires an account, video lives on their servers, a corporate Atlassian product optimized for teams, not solo use.
What I wanted: open Chrome, record, get a polished MP4. No account, no server, no subscription. So I wrote an extension.
Lesson 1: Spring physics is not overkill
The first version of zoom used linear easing. Click → camera zooms in → zooms out. Technically correct. Felt cheap.
I replaced it with a spring-based motion model rather than linear easing:
function lerpCamera(target, level, velocity, dt) {
const stiffness = 200;
const damping = 30;
const acc = (target - level) * stiffness - velocity * damping;
velocity += acc * dt;
level += velocity * dt;
return { level, velocity };
}
Critical damping here is 2 * Math.sqrt(stiffness) ≈ 28.28, so damping = 30 gives an overdamped regime: no overshoot, but with that characteristic springy ease into motion.
The difference between linear and spring is the difference between "video made in an editor" and "video shot on an iPhone." One parameter, completely different feel.
If you're building any kind of UI animation with transitions — try spring first, not easing.
Lesson 2: MV3 Offscreen Document — the extension developer's biggest headache
Chrome MV3 kills the service worker after 30 seconds of inactivity. A recording can run for 10 minutes.
The solution is the Offscreen Document: a hidden DOM context that stays alive as long as it's open. Canvas and MediaRecorder live there. The service worker just forwards events and wakes up as needed.
async function setupOffscreenDocument() {
const existing = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [chrome.runtime.getURL('recorder.html')]
});
if (existing.length > 0) return;
await chrome.offscreen.createDocument({
url: 'recorder.html',
reasons: ['USER_MEDIA'],
justification: 'Recording tab with zoom-to-cursor'
});
}
The check via chrome.runtime.getContexts is critical: an extension can only have one Offscreen Document at a time. Trying to create a second one while the first is still open throws an error. The entire recording architecture is built around this constraint — the service worker, the offscreen document, and the controls tab all pass messages in a way that guarantees a duplicate is never created.
If you're writing a Chrome extension that does background work for longer than a few seconds — Offscreen Document is the right path in MV3, but plan around this constraint from day one instead of hitting it in production.
Lesson 3: Opaque zoom makes people re-record 15-20 times
One tester told me that in their current tool, it was hard to tell when a zoom state started or ended. They often re-recorded because the result was unpredictable. He asked for some kind of hint showing when zoom starts and ends.
In Simple Screen Recorder, the zoom lasts exactly as long as the block set on the timeline — visible immediately, before export. But I never sold this in any product description. I do now.
The takeaway is simple: if a feature is controllable but the control is invisible, it behaves like randomness from the user's point of view. Visibility of state matters more than the underlying logic.
Lesson 4: Trust is a barrier before install, not after
Another tester won't install a "random plugin" without social proof that it's safe.
SSR is fully local-first — not a single byte goes to a server. But if that's not obvious at a glance, people simply don't get as far as installing it. The problem isn't the product — it's how you talk about it.
I rewrote the Chrome Web Store description so "100% local, nothing sent to a server" is the first sentence, not buried somewhere in the FAQ.
Lesson 5: A recordings library was the strongest request
A place where all recordings are saved and you can come back to them. Several testers asked for one. We didn't have it at first.
Built it with OPFS (Origin Private File System) — a browser-native file system, fully local, no server involved:
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('recording.webm', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
Recordings live in the user's browser, not on our backend — which lines up with the rest of the product's positioning (see Lesson 4).
Where it stands
v2.4.15 on the Chrome Web Store. Free, no account, Windows / Mac / Linux.
What's next: I'm currently exploring whether sharing, captions, or webcam support would be the most useful next step.
If you make demos or tutorials in the browser — simple-screen-recorder.com
And if you've built something similar and hit MV3 service worker issues — I'd love to hear how you solved them.
Tags: #chromeextension #webdev #javascript #buildinpublic
Top comments (0)