<?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: izaias</title>
    <description>The latest articles on DEV Community by izaias (@iziuqo).</description>
    <link>https://dev.to/iziuqo</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%2F4232920%2F97e65437-64aa-43d6-9986-50cd991d150f.jpg</url>
      <title>DEV Community: izaias</title>
      <link>https://dev.to/iziuqo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iziuqo"/>
    <language>en</language>
    <item>
      <title>How I built a 3.6 KB image compressor that never returns a bigger file</title>
      <dc:creator>izaias</dc:creator>
      <pubDate>Sun, 08 Nov 2026 00:10:27 +0000</pubDate>
      <link>https://dev.to/iziuqo/how-i-built-a-36-kb-image-compressor-that-never-returns-a-bigger-file-3ml6</link>
      <guid>https://dev.to/iziuqo/how-i-built-a-36-kb-image-compressor-that-never-returns-a-bigger-file-3ml6</guid>
      <description>&lt;p&gt;Every app that accepts photos runs into the same thing sooner or later. A user picks a 6 MB iPhone photo, the upload is slow, the server rejects it, or you pay to store pixels nobody will ever see.&lt;/p&gt;

&lt;p&gt;The usual fix is to compress in the browser before upload. I tried the popular libraries and kept running into three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Size.&lt;/strong&gt; 15–50 KB gzipped, for something that sits on every upload form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEIC.&lt;/strong&gt; iPhones shoot HEIC by default, and only Safari can decode it natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sometimes the "compressed" file is bigger.&lt;/strong&gt; If you re-encode an already-optimized image at quality 0.8, the result can be larger than what you started with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/iziuqo/compresso" rel="noopener noreferrer"&gt;Compresso&lt;/a&gt;&lt;/strong&gt;. Here's how it handles each of those.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4r5bh2bf5e39hw35auxh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4r5bh2bf5e39hw35auxh.gif" alt="Six photos shrink by 59–82% in the Compresso web app" width="760" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stay small: only the Canvas API
&lt;/h3&gt;

&lt;p&gt;The core uses only what the browser already has: decode into an &lt;code&gt;Image&lt;/code&gt; or &lt;code&gt;ImageBitmap&lt;/code&gt;, draw it to a canvas at the target size, and encode with &lt;code&gt;canvas.toBlob&lt;/code&gt; (or &lt;code&gt;OffscreenCanvas.convertToBlob&lt;/code&gt; in a worker). There's no WASM and no bundled codecs. The main entry is &lt;strong&gt;3.6 KB gzipped&lt;/strong&gt;, and &lt;code&gt;size-limit&lt;/code&gt; enforces that in CI, so it can't creep up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;format: 'auto'&lt;/code&gt; checks once what the browser can encode and picks &lt;strong&gt;AVIF → WebP → JPEG&lt;/strong&gt;.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compresso.js&lt;/span&gt;&lt;span class="dl"&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compress&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;savings&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;% smaller`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "87.3% smaller"&lt;/span&gt;
&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. HEIC everywhere, without paying for it
&lt;/h3&gt;

&lt;p&gt;Safari and iOS decode HEIC natively. Everywhere else, Compresso dynamically &lt;code&gt;import()&lt;/code&gt;s a WASM decoder (&lt;code&gt;heic-to&lt;/code&gt;), but only the first time a HEIC file actually shows up. Users who never pick a HEIC file never download it.&lt;/p&gt;

&lt;p&gt;Before an untyped, unnamed blob reaches the decoder, Compresso checks its &lt;code&gt;ftyp&lt;/code&gt; magic bytes. That way arbitrary data can't get there by accident.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The never-bigger guarantee
&lt;/h3&gt;

&lt;p&gt;This is the part I'm proudest of. For lossy formats, the output size is capped at the smaller of your &lt;code&gt;maxSizeMB&lt;/code&gt; and &lt;strong&gt;the original file's size&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Encode at the requested quality. If it fits, you're done.&lt;/li&gt;
&lt;li&gt;If not, &lt;strong&gt;binary-search the quality&lt;/strong&gt; for the highest value that fits under the cap.&lt;/li&gt;
&lt;li&gt;Sometimes no quality fits. For example, WebKit can only encode JPEG, but the source is a tiny AVIF. In that case Compresso &lt;strong&gt;returns the source file itself&lt;/strong&gt;, honestly labeled with its real format.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A regression test pins this behavior down, so the promise can't quietly break.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Batches off the main thread
&lt;/h3&gt;

&lt;p&gt;1.0 adds &lt;code&gt;compresso.js/pool&lt;/code&gt;, a Web Worker pool for compressing many files at once without freezing the UI:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compresso.js/pool&lt;/span&gt;&lt;span class="dl"&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;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPool&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;results&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;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compressMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Shaped like Promise.allSettled: one bad file never fails the batch.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers in the wild do odd things. iOS suspends background tabs, and a native decoder can crash. The pool notices a worker that crashed or stopped answering and replaces it. Where Workers or &lt;code&gt;OffscreenCanvas&lt;/code&gt; aren't available, or a CSP blocks them, &lt;code&gt;createPool()&lt;/code&gt; returns a main-thread version with exactly the same API, so you never branch on the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Safe by default
&lt;/h3&gt;

&lt;p&gt;A crafted file can claim enormous dimensions in a few bytes. Compresso rejects inputs above 100 megapixels by default (&lt;code&gt;maxInputPixels&lt;/code&gt;). It checks the file header before decoding, then checks again after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Live demo (nothing is uploaded): &lt;a href="https://compresso.izaias.xyz/compresso" rel="noopener noreferrer"&gt;https://compresso.izaias.xyz/compresso&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm install compresso.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Source, MIT: &lt;a href="https://github.com/iziuqo/compresso" rel="noopener noreferrer"&gt;https://github.com/iziuqo/compresso&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also powers the Compresso iPhone app. Feedback and issues are very welcome, and there are a few good first issues if you'd like to contribute.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with the help of an AI assistant.&lt;/em&gt;&lt;/p&gt;

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