<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: eugene_vo</title>
    <description>The latest articles on DEV Community by eugene_vo (@eugene_vo).</description>
    <link>https://dev.to/eugene_vo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4006472%2Fda56ef9e-5176-4e60-a6a4-81bea31e4b78.JPG</url>
      <title>DEV Community: eugene_vo</title>
      <link>https://dev.to/eugene_vo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eugene_vo"/>
    <language>en</language>
    <item>
      <title>I thought pause/resume was a basic feature. Turns out it's our main differentiator</title>
      <dc:creator>eugene_vo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:12:14 +0000</pubDate>
      <link>https://dev.to/eugene_vo/i-thought-pauseresume-was-a-basic-feature-turns-out-its-our-main-differentiator-4hp2</link>
      <guid>https://dev.to/eugene_vo/i-thought-pauseresume-was-a-basic-feature-turns-out-its-our-main-differentiator-4hp2</guid>
      <description>&lt;p&gt;One of our first beta testers — a product manager who switched to us from Cursorful — wrote this after his first recording:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pause and restart are super handy, that's really nice. Cursorful doesn't have this."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That's when you realize you're not building what you thought you were building.&lt;/p&gt;

&lt;p&gt;Five lessons from building a Chrome screen recorder — below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother at all
&lt;/h2&gt;

&lt;p&gt;If you're a designer or developer at a big company, Loom is paid for by the company, everyone uses it. No problem there.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Turns out there aren't many options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QuickTime / built-in recorder&lt;/strong&gt; — free, but no zoom API, no background compositing, no post-processing at all. Just a raw video file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Studio&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source alternatives&lt;/strong&gt; — usually mean: clone the repo, install dependencies, figure out the config. Not for someone who wants to make a quick demo between tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loom&lt;/strong&gt; — requires an account, video lives on their servers, a corporate Atlassian product optimized for teams, not solo use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted: open Chrome, record, get a polished MP4. No account, no server, no subscription. So I wrote an extension.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 1: Spring physics is not overkill
&lt;/h2&gt;

&lt;p&gt;The first version of zoom used linear easing. Click → camera zooms in → zooms out. Technically correct. Felt cheap.&lt;/p&gt;

&lt;p&gt;I replaced it with a spring-based motion model rather than linear easing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lerpCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stiffness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;damping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;stiffness&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;velocity&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critical damping here is &lt;code&gt;2 * Math.sqrt(stiffness)&lt;/code&gt; ≈ 28.28, so &lt;code&gt;damping = 30&lt;/code&gt; gives an overdamped regime: no overshoot, but with that characteristic springy ease into motion.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you're building any kind of UI animation with transitions — try spring first, not easing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 2: MV3 Offscreen Document — the extension developer's biggest headache
&lt;/h2&gt;

&lt;p&gt;Chrome MV3 kills the service worker after 30 seconds of inactivity. A recording can run for 10 minutes.&lt;/p&gt;

&lt;p&gt;The solution is the &lt;strong&gt;Offscreen Document&lt;/strong&gt;: 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupOffscreenDocument&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContexts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;contextTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OFFSCREEN_DOCUMENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;documentUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recorder.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offscreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recorder.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USER_MEDIA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recording tab with zoom-to-cursor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check via &lt;code&gt;chrome.runtime.getContexts&lt;/code&gt; is critical: an extension can only have &lt;strong&gt;one Offscreen Document at a time&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 3: Opaque zoom makes people re-record 15-20 times
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 4: Trust is a barrier before install, not after
&lt;/h2&gt;

&lt;p&gt;Another tester won't install a "random plugin" without social proof that it's safe.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 5: A recordings library was the strongest request
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Built it with &lt;strong&gt;OPFS&lt;/strong&gt; (Origin Private File System) — a browser-native file system, fully local, no server involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileHandle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFileHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recording.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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).&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;v2.4.15 on the Chrome Web Store. Free, no account, Windows / Mac / Linux.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next:&lt;/strong&gt; I'm currently exploring whether sharing, captions, or webcam support would be the most useful next step.&lt;/p&gt;

&lt;p&gt;If you make demos or tutorials in the browser — &lt;a href="https://simple-screen-recorder.com" rel="noopener noreferrer"&gt;simple-screen-recorder.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you've built something similar and hit MV3 service worker issues — I'd love to hear how you solved them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #chromeextension #webdev #javascript #buildinpublic&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>product</category>
      <category>sideprojects</category>
      <category>ux</category>
    </item>
  </channel>
</rss>
