<?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: Convertilo</title>
    <description>The latest articles on DEV Community by Convertilo (@convertilo).</description>
    <link>https://dev.to/convertilo</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3941096%2Fe9bb21dc-8d70-4a28-beba-c9e1dfe3fb61.png</url>
      <title>DEV Community: Convertilo</title>
      <link>https://dev.to/convertilo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/convertilo"/>
    <language>en</language>
    <item>
      <title>I benchmarked 6 WASM image codecs in the browser. Here is what beats the server.</title>
      <dc:creator>Convertilo</dc:creator>
      <pubDate>Tue, 19 May 2026 23:07:41 +0000</pubDate>
      <link>https://dev.to/convertilo/i-benchmarked-6-wasm-image-codecs-in-the-browser-here-is-what-beats-the-server-1dlb</link>
      <guid>https://dev.to/convertilo/i-benchmarked-6-wasm-image-codecs-in-the-browser-here-is-what-beats-the-server-1dlb</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Modern WebAssembly codecs (MozJPEG, libavif, OxiPNG/imagequant, libwebp, gifsicle, SVGO) now run fast enough in the browser that you can ship a real image-compression tool &lt;strong&gt;without uploading anything to a server&lt;/strong&gt;. I built one (&lt;a href="https://convertilo.io" rel="noopener noreferrer"&gt;convertilo.io&lt;/a&gt;) and benchmarked the codec mix on 100 real-world images. Results below — and a couple of surprises about what &lt;em&gt;doesn't&lt;/em&gt; work.&lt;/p&gt;

&lt;p&gt;Raw data + repo: &lt;a href="https://github.com/convertilo/wasm-image-benchmarks" rel="noopener noreferrer"&gt;github.com/convertilo/wasm-image-benchmarks&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Corpus: 100 mixed real-world images (photos, screenshots, transparent PNGs, animated GIFs, vector icons)&lt;/li&gt;
&lt;li&gt;Quality: 75 (sane default for lossy)&lt;/li&gt;
&lt;li&gt;Environment: Chrome 130, M-series Mac, warm WASM cache&lt;/li&gt;
&lt;li&gt;Median reduction (per-image variance is ~±15%)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Median size reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;MozJPEG (@jsquash/jpeg)&lt;/td&gt;
&lt;td&gt;lossy q=75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−53%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;imagequant + @jsquash/png&lt;/td&gt;
&lt;td&gt;lossy palette&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−75%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;OxiPNG (@jsquash/png)&lt;/td&gt;
&lt;td&gt;lossless&lt;/td&gt;
&lt;td&gt;varies*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;libwebp (@jsquash/webp)&lt;/td&gt;
&lt;td&gt;lossy re-encode&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−17%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;libavif (@jsquash/avif)&lt;/td&gt;
&lt;td&gt;lossy q=75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−59%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF (static)&lt;/td&gt;
&lt;td&gt;gifsicle-wasm-browser&lt;/td&gt;
&lt;td&gt;lossy −O3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−75%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF (animated)&lt;/td&gt;
&lt;td&gt;gifsicle-wasm-browser&lt;/td&gt;
&lt;td&gt;lossy −O3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−27%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;SVGO v4 browser bundle&lt;/td&gt;
&lt;td&gt;lossless multipass&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−42%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*OxiPNG depends on whether the input was already optimized — anything from 0% to 60%.&lt;/p&gt;

&lt;p&gt;PNG via imagequant and JPEG via MozJPEG hit numbers competitive with &lt;strong&gt;server-side TinyPNG&lt;/strong&gt;. That was the moment I stopped looking for a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;@jsquash&lt;/strong&gt; — Sam Sneddon's WASM bindings around mozjpeg, libwebp, libavif, OxiPNG, imagequant. Lazy-loaded per format so the bundle stays tiny: ~250 KB only when the user picks a real file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gifsicle-wasm-browser&lt;/strong&gt; — full gifsicle 1.92 CLI compiled to WASM. Preserves animation frames properly (more on this below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVGO v4&lt;/strong&gt; — works fine in the browser via the bundled &lt;code&gt;svgo/browser&lt;/code&gt; import. No need for a server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three things that didn't work (and saved you the time)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  imagequant for WebP/JPEG
&lt;/h3&gt;

&lt;p&gt;imagequant gives PNG the −75% number, so my first instinct was "apply the same quantization to WebP/JPEG before encoding." It actively &lt;strong&gt;hurts&lt;/strong&gt; there: the palette quantization introduces dithering noise that wrecks lossy entropy coding. WebP went from −17% to &lt;strong&gt;−12%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rule: imagequant for lossless palette PNG only. Don't cross-apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebP knob-twisting
&lt;/h3&gt;

&lt;p&gt;I sunk a few hours into &lt;code&gt;method: 4&lt;/code&gt;, &lt;code&gt;pass: 3&lt;/code&gt;, &lt;code&gt;sns_strength: 75&lt;/code&gt;, &lt;code&gt;use_sharp_yuv: 1&lt;/code&gt; for libwebp. Total impact: &amp;lt;1% extra reduction on already-optimized WebPs. If you're re-encoding WebP that's been touched by another tool, you're squeezing a stone.&lt;/p&gt;

&lt;h3&gt;
  
  
  GIF via canvas/ImageData
&lt;/h3&gt;

&lt;p&gt;I tried decoding GIF, processing as ImageData, re-encoding. Animation frame timings die in transit. Just use gifsicle as a File → File pipeline and pass &lt;code&gt;-O3 --lossy=80&lt;/code&gt; directly. Don't reimplement what's already a 30-year-old C tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy-first angle
&lt;/h2&gt;

&lt;p&gt;The reason I cared at all: image compressors historically upload your files. Receipts, IDs, family photos, contracts — your stuff sits on someone's server. With WASM you can run the &lt;strong&gt;same codecs&lt;/strong&gt; (MozJPEG, libavif, OxiPNG) entirely in the user's tab. Nothing leaves the device.&lt;/p&gt;

&lt;p&gt;I learned the hard way that this also requires &lt;strong&gt;not&lt;/strong&gt; loading Yandex.Metrika or Google Analytics before consent, which is a whole other post. The codec part is the easy half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/convertilo/wasm-image-benchmarks
&lt;span class="nb"&gt;cd &lt;/span&gt;wasm-image-benchmarks
&lt;span class="c"&gt;# results.json has the medians I cite above&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;results.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want a live implementation rather than a benchmark, all of these codecs are wired up at &lt;a href="https://convertilo.io" rel="noopener noreferrer"&gt;convertilo.io&lt;/a&gt; — drop a file in and watch network: no upload happens.&lt;/p&gt;




&lt;p&gt;If you've squeezed more out of any of these (especially WebP — I'm convinced there's more there), I'd love the numbers. Drop a comment.&lt;/p&gt;

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