<?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: DVD Screensaver Maker</title>
    <description>The latest articles on DEV Community by DVD Screensaver Maker (@dvdscreensavermaker).</description>
    <link>https://dev.to/dvdscreensavermaker</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%2F4022068%2F4977e69e-e3f0-477b-a097-d11c548e6b7a.jpg</url>
      <title>DEV Community: DVD Screensaver Maker</title>
      <link>https://dev.to/dvdscreensavermaker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dvdscreensavermaker"/>
    <language>en</language>
    <item>
      <title>My Web Worker Wrote Video Straight to Disk. Then It Started Leaving Bodies. 🧟</title>
      <dc:creator>DVD Screensaver Maker</dc:creator>
      <pubDate>Sat, 25 Jul 2026 15:12:32 +0000</pubDate>
      <link>https://dev.to/dvdscreensavermaker/my-web-worker-wrote-video-straight-to-disk-then-it-started-leaving-bodies-5abc</link>
      <guid>https://dev.to/dvdscreensavermaker/my-web-worker-wrote-video-straight-to-disk-then-it-started-leaving-bodies-5abc</guid>
      <description>&lt;p&gt;I'm learning AI engineering by pairing with Claude on real projects, and my current one is a hobby app: &lt;a href="https://dvdscreensavermaker.com" rel="noopener noreferrer"&gt;📀 DVD Screensaver Maker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's a browser-based bouncing DVD logo simulator. You can fully customize — colors, speed, shapes, and more — then export as HTML, GIF, or MP4/WebM. No signup, no watermark, completely free.&lt;/p&gt;

&lt;p&gt;For MP4/WebM specifically, every last frame renders and encodes right there on your machine — not a nice-to-have bolted on later, but the whole point. 🧐&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/otYSePToNOE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;That's the actual export, straight off the site above — untouched, unedited.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why was "everything client-side" non-negotiable?
&lt;/h3&gt;

&lt;p&gt;Two reasons, neither of them "because it's cool":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pitch is "nothing leaves your browser."&lt;/strong&gt; A server-side encode step breaks that promise on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is a hobby project.&lt;/strong&gt; I'm not paying for GPU encoding infrastructure so strangers can watch a logo bounce — my hosting bill stays basically $0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So everything had to happen in the browser, including the heavy part — encoding the video — and that decision sent me straight down the rabbit hole. 🐰&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbti548ebmr81put20soo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbti548ebmr81put20soo.gif" alt="how hard could it be" width="480" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem: one long export away from crashing the tab
&lt;/h3&gt;

&lt;p&gt;The obvious design goes like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encode frames&lt;/strong&gt; (&lt;code&gt;VideoEncoder&lt;/code&gt;) → &lt;strong&gt;Mux&lt;/strong&gt; (&lt;code&gt;webm-muxer&lt;/code&gt; / &lt;code&gt;mp4-muxer&lt;/code&gt;) → &lt;strong&gt;Hold entire result in memory&lt;/strong&gt; → &lt;strong&gt;Hand user one big &lt;code&gt;Blob&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both muxer libraries default to exactly this — an in-memory &lt;code&gt;ArrayBufferTarget&lt;/code&gt; — and it works great for a 5-second clip.&lt;/p&gt;

&lt;p&gt;But someone will eventually export 1080p for a full minute, and now the tab is holding a multi-gigabyte &lt;code&gt;ArrayBuffer&lt;/code&gt; hostage.&lt;/p&gt;

&lt;p&gt;Best case, your fan spins up like it's taking off 🚁. Worst case, the tab just dies 💀. &lt;/p&gt;

&lt;p&gt;No error, no toast, just silence — and a user left wondering if &lt;em&gt;they&lt;/em&gt; broke something. &lt;/p&gt;

&lt;p&gt;(They didn't. I did.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma1sxguq3dzm0tf9nnbk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma1sxguq3dzm0tf9nnbk.gif" alt="tab crash" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: stream straight to disk with OPFS
&lt;/h3&gt;

&lt;p&gt;Turns out both muxer libraries accept a &lt;code&gt;FileSystemWritableFileStreamTarget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of building the video in RAM, give them a &lt;em&gt;file&lt;/em&gt; and write each encoded chunk straight to disk as it's produced.&lt;/p&gt;

&lt;p&gt;The disk in question is the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system" rel="noopener noreferrer"&gt;Origin Private File System&lt;/a&gt; (OPFS).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a sandboxed filesystem the browser gives your site.&lt;/li&gt;
&lt;li&gt;It's invisible to the user.&lt;/li&gt;
&lt;li&gt;Only your own JS can touch it.&lt;/li&gt;
&lt;li&gt;Crucially: it's still 100% client-side. No server involved.
&lt;/li&gt;
&lt;/ul&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="nx"&gt;filename&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;new&lt;/span&gt; &lt;span class="nc"&gt;Mp4Muxer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mp4FileTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;avc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frameRate&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;fastStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// moov atom at the END — no need to know file size up front&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: memory usage stays roughly &lt;strong&gt;constant&lt;/strong&gt; no matter how long the export runs — a 1-hour 1080p export puts about the same RAM pressure on the tab as a 5-second one. 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5sf3djinm1csopvl7wbp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5sf3djinm1csopvl7wbp.gif" alt="finally relief" width="500" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Plot twist: now I had zombie files
&lt;/h3&gt;

&lt;p&gt;Streaming to disk solved the memory problem. It also created a new one, because it turns out &lt;strong&gt;OPFS is forever.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tab closes mid-export? &lt;strong&gt;File stays.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Browser crashes? &lt;strong&gt;File stays.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;User wanders off and abandons the modal? &lt;strong&gt;File stays.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way: the file just... sits there. Forever. 🧟&lt;/p&gt;

&lt;p&gt;Every abandoned export becomes a half-finished MP4 ghost, haunting the user's disk quota, never blinking, never leaving, never explaining itself. A tiny immortal freeloader I created and then forgot about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbeq2byh7c9wih8l6cil.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbeq2byh7c9wih8l6cil.gif" alt="zombie files" width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now I needed two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A way to know: &lt;em&gt;is this file still being actively written by a live tab?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;A way to clean up files left behind by tabs that are no longer around to clean up after themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enter the Web Locks API
&lt;/h3&gt;

&lt;p&gt;Here's the trick. Every export grabs a named lock: &lt;code&gt;opfs:&amp;lt;filename&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It holds that lock for the file's entire lifetime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;encoding&lt;/li&gt;
&lt;li&gt;ready-to-download&lt;/li&gt;
&lt;li&gt;a final size-scaled grace period after the download starts, before I finally delete the file (more on that below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On page load, every tab runs a little sweep:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List all files currently in OPFS.&lt;/li&gt;
&lt;li&gt;Try to grab each file's lock with &lt;code&gt;{ ifAvailable: true }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Got the lock?&lt;/strong&gt; Nobody's using that file — delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Couldn't get it?&lt;/strong&gt; A live tab is holding it — leave it alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a tab dies its lock releases automatically, so every other tab's startup sweep self-heals the system with no cleanup code, dialog, or cron job required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxox0y3v2e4phgddl9xuh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxox0y3v2e4phgddl9xuh.gif" alt="automatic cleanup" width="478" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Okay but how does a sandboxed file become a download?
&lt;/h3&gt;

&lt;p&gt;Here's a detail I glossed over: OPFS is invisible. Your devtools barely show it. The user's OS definitely can't see it. &lt;/p&gt;

&lt;p&gt;So even after the muxer finishes writing a perfectly good MP4 to disk, that file is still trapped inside the browser's private little vault.&lt;/p&gt;

&lt;p&gt;Getting it out is almost anticlimactic:&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;file&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;getFile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// OPFS handle -&amp;gt; real File object&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// give it a URL the page can use&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                          &lt;span class="c1"&gt;// let the browser do its normal download thing&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;showSaveFilePicker&lt;/code&gt;. No service worker trickery. No streaming the bytes anywhere. &lt;/p&gt;

&lt;p&gt;Just: ask OPFS for the finished &lt;code&gt;File&lt;/code&gt;, wrap it in an object URL, fake-click an &lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt;, and let the browser's completely ordinary download machinery take it from there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31tjmfirk4k8rs5g1pa9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31tjmfirk4k8rs5g1pa9.gif" alt="pulling the file out" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The part where I almost deleted the file mid-download
&lt;/h3&gt;

&lt;p&gt;Here's the trap. Clicking the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; doesn't mean the download is &lt;em&gt;done&lt;/em&gt;. It means the browser has &lt;em&gt;started&lt;/em&gt; copying that object-URL blob out to disk. &lt;/p&gt;

&lt;p&gt;For a 5-second clip, that's instant. For a 20-minute 1080p export, that copy can take a few seconds — sometimes longer on a slow disk.&lt;/p&gt;

&lt;p&gt;If I revoke the object URL and delete the OPFS file the moment &lt;code&gt;.click()&lt;/code&gt; returns, I've pulled the file out from under a download that's still in progress. &lt;/p&gt;

&lt;p&gt;Best case, nothing happens. Worst case, the user gets a corrupted, truncated video and no idea why.&lt;/p&gt;

&lt;p&gt;So the cleanup has to wait. I keep the file's lock held, then delay the delete by an amount that scales with file size:&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;safeDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;releaseLock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;safeDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;20 seconds minimum, plus roughly another second per 50MB, plus a 15-second cushion on top. &lt;/p&gt;

&lt;p&gt;It's not a precise measurement of "when is the download actually finished" — there's no browser API that tells you that. It's just "give it enough rope." ➰&lt;/p&gt;

&lt;p&gt;Only when that timer fires do I revoke the URL, delete the file, and release the lock — the same lock the startup sweep checks, as a safety net if this tab never gets that far.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokp1fotoru2i0cwv0rpa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokp1fotoru2i0cwv0rpa.gif" alt="waiting it out" width="330" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Was it worth it?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes. But it wasn't free.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The good:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Constant memory usage, regardless of export length or resolution.&lt;/li&gt;
&lt;li&gt;✅ No more crash-prone giant in-memory buffers.&lt;/li&gt;
&lt;li&gt;✅ Still 100% client-side — no server, no upload, no bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Went from "encode → hand back a Blob" to file handles, lock coordination, and a startup sweep — a lot of moving parts for what sounds like a one-line change.&lt;/li&gt;
&lt;li&gt;❌ OPFS support isn't universal, so you need a feature check (&lt;code&gt;navigator.storage.getDirectory&lt;/code&gt;) and a fallback plan.&lt;/li&gt;
&lt;li&gt;❌ Debugging got &lt;em&gt;weirder&lt;/em&gt; — no more &lt;code&gt;console.log&lt;/code&gt;-ing a Blob, just files in a sandboxed filesystem your devtools barely show you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a toy project I built for fun, this is probably more engineering than the situation strictly demanded.&lt;/p&gt;

&lt;p&gt;But "the tab silently dies on a big export" isn't a bug you get to ship. Even in a for-fun side project. 🙃&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzc2agqiaqfmw6qdta97.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzc2agqiaqfmw6qdta97.gif" alt="ship it" width="480" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it (and hopefully don't crash your tab):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dvdscreensavermaker.com" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdvdscreensavermaker.com%2Fog-image.png" alt="DVD Screensaver Maker" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
