<?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: WhizTools.app</title>
    <description>The latest articles on DEV Community by WhizTools.app (@whiztools_app).</description>
    <link>https://dev.to/whiztools_app</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%2F4039943%2F22914372-83cc-4c14-be77-87a378e42845.png</url>
      <title>DEV Community: WhizTools.app</title>
      <link>https://dev.to/whiztools_app</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whiztools_app"/>
    <language>en</language>
    <item>
      <title>Replacing ffmpeg.wasm with WebCodecs: a 1080p export from 10.3s to 2.6s</title>
      <dc:creator>WhizTools.app</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:28:32 +0000</pubDate>
      <link>https://dev.to/whiztools_app/replacing-ffmpegwasm-with-webcodecs-a-1080p-export-from-103s-to-26s-40b8</link>
      <guid>https://dev.to/whiztools_app/replacing-ffmpegwasm-with-webcodecs-a-1080p-export-from-103s-to-26s-40b8</guid>
      <description>&lt;p&gt;I maintain a browser-based video editor. Everything runs client-side — no upload, no server — which for a long time meant ffmpeg compiled to WebAssembly doing the export.&lt;/p&gt;

&lt;p&gt;It works. It's just slow. ffmpeg.wasm encodes around &lt;strong&gt;30 frames a second at 1080p&lt;/strong&gt;, so a ten-second clip takes about ten seconds of watching a progress bar. Not because ffmpeg is slow — because it's a copy of an encoder running in a sandbox, without access to anything the platform already has.&lt;/p&gt;

&lt;p&gt;The browser &lt;em&gt;does&lt;/em&gt; have a real encoder. &lt;code&gt;VideoEncoder&lt;/code&gt; is the same one it uses for WebRTC. Here's what swapping to it involved, and the three bugs that cost me most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ffmpeg.wasm    WebCodecs
  720p  10s      7.7s         2.0s      3.9x
 1080p  10s     10.3s         2.6s      4.0x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is pixel-identical to the ffmpeg path — same compositing code, just fed differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;Four stages, all of them streaming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mp4box  →  EncodedVideoChunk  →  VideoDecoder  →  VideoFrame
                                                     ↓
                                          composite offscreen
                                                     ↓
                                   VideoEncoder  →  mux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Demuxing.&lt;/strong&gt; &lt;code&gt;VideoDecoder&lt;/code&gt; wants encoded chunks, and a browser won't give you those from a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element. &lt;a href="https://github.com/gpac/mp4box.js" rel="noopener noreferrer"&gt;mp4box.js&lt;/a&gt; parses the container and hands you samples. The part that isn't obvious: the decoder also needs the codec-private data, and that doesn't live in the samples — it's in the sample description box.&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;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mp4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;track&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;trak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mp4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTrackById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &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;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;trak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mdia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stbl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stsd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&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;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avcC&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hvcC&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;av1C&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpcC&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BIG_ENDIAN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;box&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;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// first eight bytes are the box header, which the decoder does not want&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&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;Miss that &lt;code&gt;, 8&lt;/code&gt; and the decoder rejects the config with a message that tells you nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compositing.&lt;/strong&gt; I didn't write a second renderer. The editor's preview already draws every clip, title, transition and colour adjustment to a canvas. For export I run &lt;em&gt;that same code&lt;/em&gt; offscreen at output resolution, with its frame source swapped from &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; elements to decoded &lt;code&gt;VideoFrame&lt;/code&gt;s. Which means the export looks like the preview by construction, rather than by me keeping two renderers in sync for ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the decoder stops if you hold its output
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VideoDecoder&lt;/code&gt; applies back-pressure. If you don't close the &lt;code&gt;VideoFrame&lt;/code&gt;s it gives you, it stops producing. My first feeder queued decoded frames generously — eight in flight, plenty buffered — and the export hung about four seconds in, every time.&lt;/p&gt;

&lt;p&gt;The decoder was waiting for me to release frames. I was waiting for the decoder to produce one more. Neither moved.&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunks&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;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodeQueueSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
       &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&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;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="o"&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;Shallow queues. Counter-intuitive if you're used to buffering being free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: setTimeout(0) is not zero
&lt;/h2&gt;

&lt;p&gt;The frame loop yields to let the decoder run. I used &lt;code&gt;setTimeout(0)&lt;/code&gt;, as you do.&lt;/p&gt;

&lt;p&gt;Browsers clamp nested timers. In a real page that yield costs about &lt;strong&gt;7 ms&lt;/strong&gt;. Over an export that yields tens of thousands of times, that was most of the runtime I'd supposedly saved.&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;yieldToTasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MessageChannel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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;&lt;strong&gt;0.04 ms&lt;/strong&gt;, and not clamped. Same semantics, 175x cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: one character
&lt;/h2&gt;

&lt;p&gt;The loop asks "does the frame I'm holding cover the moment I need?" I wrote:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A frame at 66,666 µs lasting 33,333 µs covers exactly up to 99,999 — which is precisely the moment the next output frame asks for. With a strict &lt;code&gt;&amp;gt;&lt;/code&gt;, it could neither advance nor finish. It waited for ever, on some files and not others, depending on where their timestamps started.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;. One character, an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually found it&lt;/strong&gt; was giving up on guessing and making the timeout print its own state:&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The decoder stopped producing frames. &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`[want=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; frame=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; queue=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`at=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; inflight=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodeQueueSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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 numbers made it obvious in seconds. I should have done it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No hardware encoder.&lt;/strong&gt; &lt;code&gt;VideoEncoder&lt;/code&gt; doesn't reach your GPU's dedicated encoder, so desktop ffmpeg is still faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container support is the browser's.&lt;/strong&gt; ProRes, some MKVs, HEVC in some browsers — those fall back to the ffmpeg path, which is why I kept it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is the tab's.&lt;/strong&gt; No server to offload to. That's the trade you make for not uploading anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Worth it?
&lt;/h2&gt;

&lt;p&gt;For a browser-only editor, yes — four times faster on the operation users actually wait for. And the fallback matters: WebCodecs where it works, ffmpeg.wasm where it doesn't, decided per file.&lt;/p&gt;

&lt;p&gt;If you're doing something similar: shallow queues, &lt;code&gt;MessageChannel&lt;/code&gt; not &lt;code&gt;setTimeout&lt;/code&gt;, and make your timeouts print their own state before you start guessing.&lt;/p&gt;

&lt;p&gt;The editor is at &lt;a href="https://whiztools.app/video-editor" rel="noopener noreferrer"&gt;whiztools.app/video-editor&lt;/a&gt; if you want to throw a file at it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
    <item>
      <title>Building an AI Image Upscaler That Runs Entirely in the Browser (No Server Required)</title>
      <dc:creator>WhizTools.app</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:55:20 +0000</pubDate>
      <link>https://dev.to/whiztools_app/building-an-ai-image-upscaler-that-runs-entirely-in-the-browser-no-server-required-59cm</link>
      <guid>https://dev.to/whiztools_app/building-an-ai-image-upscaler-that-runs-entirely-in-the-browser-no-server-required-59cm</guid>
      <description>&lt;p&gt;AI image upscalers are everywhere these days.&lt;/p&gt;

&lt;p&gt;Most of them work the same way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload your image.&lt;/li&gt;
&lt;li&gt;Wait while it's processed on a remote server.&lt;/li&gt;
&lt;li&gt;Download the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works—but it also means you're sending your photos, screenshots, or documents to someone else's infrastructure.&lt;/p&gt;

&lt;p&gt;I wanted to see if I could build an alternative that keeps everything on the user's device.&lt;/p&gt;

&lt;p&gt;The result is a &lt;strong&gt;fully browser-based AI Image Upscaler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No uploads.&lt;/p&gt;

&lt;p&gt;No server processing.&lt;/p&gt;

&lt;p&gt;No account required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build It This Way?
&lt;/h2&gt;

&lt;p&gt;I've been building a collection of browser-based utilities over the past few months, and one of the most requested features from the community was image upscaling.&lt;/p&gt;

&lt;p&gt;The obvious solution would have been to process images on a backend.&lt;/p&gt;

&lt;p&gt;Instead, I challenged myself to keep the entire pipeline inside the browser.&lt;/p&gt;

&lt;p&gt;That meant users could enhance images without them ever leaving their device.&lt;/p&gt;

&lt;p&gt;Besides privacy, this has a few practical benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔒 Your images stay on your computer.&lt;/li&gt;
&lt;li&gt;⚡ No waiting for uploads before processing begins.&lt;/li&gt;
&lt;li&gt;🚫 No account or sign-up required.&lt;/li&gt;
&lt;li&gt;🌐 Works even with a slow or unstable connection once the application is loaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenges
&lt;/h2&gt;

&lt;p&gt;Building an AI-powered feature entirely in the browser wasn't as straightforward as I expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading AI Models
&lt;/h3&gt;

&lt;p&gt;AI models aren't small.&lt;/p&gt;

&lt;p&gt;Loading them efficiently without making users wait forever was one of the biggest challenges.&lt;/p&gt;

&lt;p&gt;The first experience matters, so I spent time reducing unnecessary loading and making the application feel responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory Usage
&lt;/h3&gt;

&lt;p&gt;Upscaling large images can quickly consume a lot of memory.&lt;/p&gt;

&lt;p&gt;Modern browsers are powerful, but they're still running inside a constrained environment.&lt;/p&gt;

&lt;p&gt;Finding the right balance between image quality, speed, and memory usage took quite a bit of experimentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping the UI Responsive
&lt;/h3&gt;

&lt;p&gt;Nobody likes a frozen browser tab.&lt;/p&gt;

&lt;p&gt;Long-running image processing needs to happen without making the interface feel sluggish.&lt;/p&gt;

&lt;p&gt;Keeping the application responsive while processing large images was an important part of the implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile Performance
&lt;/h3&gt;

&lt;p&gt;Desktop computers generally have enough resources for image processing.&lt;/p&gt;

&lt;p&gt;Mobile devices are a different story.&lt;/p&gt;

&lt;p&gt;Different hardware, limited memory, and thermal constraints meant I had to test across multiple devices to make sure the experience remained usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Technology Has Come a Long Way
&lt;/h2&gt;

&lt;p&gt;One thing this project reinforced is how capable modern browsers have become.&lt;/p&gt;

&lt;p&gt;A few years ago, I would have assumed this kind of feature required cloud infrastructure.&lt;/p&gt;

&lt;p&gt;Today, browsers can handle surprisingly complex workloads while still delivering a good user experience.&lt;/p&gt;

&lt;p&gt;It opens up interesting possibilities for building applications that are both privacy-friendly and performant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;This project reminded me that not every feature needs a backend.&lt;/p&gt;

&lt;p&gt;Sometimes the browser is more than capable of doing the heavy lifting.&lt;/p&gt;

&lt;p&gt;Choosing client-side processing whenever it makes sense can improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User privacy&lt;/li&gt;
&lt;li&gt;Perceived performance&lt;/li&gt;
&lt;li&gt;Simplicity&lt;/li&gt;
&lt;li&gt;User trust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, browser-based AI isn't the answer for every use case, but for many everyday utilities it's a compelling approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd Love Your Feedback
&lt;/h2&gt;

&lt;p&gt;I'm continuing to improve the image quality and performance based on real-world usage.&lt;/p&gt;

&lt;p&gt;If you've built AI features that run entirely in the browser, I'd love to hear about your experience.&lt;/p&gt;

&lt;p&gt;And if you're interested in trying it yourself, you can check it out here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://whiztools.app/upscale-image" rel="noopener noreferrer"&gt;https://whiztools.app/upscale-image&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm especially interested in feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image quality&lt;/li&gt;
&lt;li&gt;Processing speed&lt;/li&gt;
&lt;li&gt;Large image support&lt;/li&gt;
&lt;li&gt;Mobile experience&lt;/li&gt;
&lt;li&gt;Browser compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! If you have suggestions or ideas for improving browser-based AI tools, I'd love to discuss them in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Stopped Uploading My Files to Random Websites. So I Built My Own Solution.</title>
      <dc:creator>WhizTools.app</dc:creator>
      <pubDate>Tue, 21 Jul 2026 11:11:34 +0000</pubDate>
      <link>https://dev.to/whiztools_app/i-built-100-browser-based-tools-that-never-upload-your-files-2fhi</link>
      <guid>https://dev.to/whiztools_app/i-built-100-browser-based-tools-that-never-upload-your-files-2fhi</guid>
      <description>&lt;p&gt;How many times have you done something like this?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge a PDF&lt;/li&gt;
&lt;li&gt;Compress an image&lt;/li&gt;
&lt;li&gt;Convert a PNG to JPG&lt;/li&gt;
&lt;li&gt;Format a JSON file&lt;/li&gt;
&lt;li&gt;Generate a QR code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You search Google, click the first result, upload your file, wait a few seconds, download the output, and hope the website actually deletes your data.&lt;/p&gt;

&lt;p&gt;For years, I never questioned this workflow.&lt;/p&gt;

&lt;p&gt;Then one day I asked myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why am I uploading local files for tasks that my browser should already be capable of doing?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single question turned into a side project that eventually grew into &lt;strong&gt;100+ browser-based tools&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Browser Has Quietly Become Incredibly Powerful
&lt;/h2&gt;

&lt;p&gt;Modern browsers can do far more than most developers realize.&lt;/p&gt;

&lt;p&gt;Today, they can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process images&lt;/li&gt;
&lt;li&gt;Manipulate PDFs&lt;/li&gt;
&lt;li&gt;Generate QR codes&lt;/li&gt;
&lt;li&gt;Read barcodes&lt;/li&gt;
&lt;li&gt;Perform OCR&lt;/li&gt;
&lt;li&gt;Format JSON&lt;/li&gt;
&lt;li&gt;Compress files&lt;/li&gt;
&lt;li&gt;Work with the Clipboard API&lt;/li&gt;
&lt;li&gt;Access local files securely&lt;/li&gt;
&lt;li&gt;Store data offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet thousands of websites still upload everything to a backend.&lt;/p&gt;

&lt;p&gt;Sometimes that's necessary.&lt;/p&gt;

&lt;p&gt;But many times... it isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Upload Everything" Habit
&lt;/h2&gt;

&lt;p&gt;I understand why many online tools work this way.&lt;/p&gt;

&lt;p&gt;Server-side processing is easier.&lt;/p&gt;

&lt;p&gt;You have full control over the environment.&lt;/p&gt;

&lt;p&gt;Libraries are mature.&lt;/p&gt;

&lt;p&gt;Performance is predictable.&lt;/p&gt;

&lt;p&gt;But there's a trade-off.&lt;/p&gt;

&lt;p&gt;Every upload means your users have to trust your infrastructure with their personal files.&lt;/p&gt;

&lt;p&gt;Whether it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tax documents&lt;/li&gt;
&lt;li&gt;Contracts&lt;/li&gt;
&lt;li&gt;Family photos&lt;/li&gt;
&lt;li&gt;Screenshots&lt;/li&gt;
&lt;li&gt;IDs&lt;/li&gt;
&lt;li&gt;Business documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;many people would rather those files never leave their own computer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy Shouldn't Be a Premium Feature
&lt;/h2&gt;

&lt;p&gt;One thing surprised me after launching.&lt;/p&gt;

&lt;p&gt;Almost nobody commented on the number of tools.&lt;/p&gt;

&lt;p&gt;Instead, people kept saying things like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Finally, something that doesn't require an account."&lt;/p&gt;

&lt;p&gt;"I love that everything runs locally."&lt;/p&gt;

&lt;p&gt;"Instant bookmark."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That made me realize something important.&lt;/p&gt;

&lt;p&gt;People don't just want convenience.&lt;/p&gt;

&lt;p&gt;They also want control.&lt;/p&gt;

&lt;p&gt;Sometimes the best feature isn't AI.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;not uploading the file at all.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  It Wasn't Easy
&lt;/h2&gt;

&lt;p&gt;Building everything inside the browser introduced challenges I hadn't expected.&lt;/p&gt;

&lt;p&gt;Large files can easily consume memory.&lt;/p&gt;

&lt;p&gt;Different browsers behave differently.&lt;/p&gt;

&lt;p&gt;OCR running locally is much harder than using a cloud API.&lt;/p&gt;

&lt;p&gt;Mobile devices have much stricter resource limits.&lt;/p&gt;

&lt;p&gt;Every feature became a balancing act between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Browser compatibility&lt;/li&gt;
&lt;li&gt;User experience&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I Kept Going
&lt;/h2&gt;

&lt;p&gt;Because every time I removed one unnecessary upload...&lt;/p&gt;

&lt;p&gt;the experience became simpler.&lt;/p&gt;

&lt;p&gt;Open the page.&lt;/p&gt;

&lt;p&gt;Choose a file.&lt;/p&gt;

&lt;p&gt;Process it.&lt;/p&gt;

&lt;p&gt;Download the result.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;No account.&lt;/p&gt;

&lt;p&gt;No waiting for uploads.&lt;/p&gt;

&lt;p&gt;No unnecessary permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;That idea slowly grew into a collection of &lt;strong&gt;100+ browser-based tools&lt;/strong&gt; covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF utilities&lt;/li&gt;
&lt;li&gt;Image tools&lt;/li&gt;
&lt;li&gt;OCR&lt;/li&gt;
&lt;li&gt;QR &amp;amp; Barcode generators&lt;/li&gt;
&lt;li&gt;Text utilities&lt;/li&gt;
&lt;li&gt;Developer tools&lt;/li&gt;
&lt;li&gt;Color tools&lt;/li&gt;
&lt;li&gt;Security &amp;amp; encoding tools&lt;/li&gt;
&lt;li&gt;Calculators&lt;/li&gt;
&lt;li&gt;Productivity tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of them process everything &lt;strong&gt;locally inside the browser&lt;/strong&gt;, and many continue to work &lt;strong&gt;offline after the initial page load&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;As developers, we often chase bigger features.&lt;/p&gt;

&lt;p&gt;Bigger infrastructure.&lt;/p&gt;

&lt;p&gt;More APIs.&lt;/p&gt;

&lt;p&gt;More backend services.&lt;/p&gt;

&lt;p&gt;Sometimes the better product comes from removing complexity instead.&lt;/p&gt;

&lt;p&gt;Modern browsers are capable of much more than we give them credit for.&lt;/p&gt;

&lt;p&gt;Maybe it's time we started using that power.&lt;/p&gt;




&lt;p&gt;If you're interested in browser-first utilities, you can check out what I've been building:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://whiztools.app" rel="noopener noreferrer"&gt;https://whiztools.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd also love to hear your thoughts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What everyday tool do you wish could run entirely in the browser?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
