<?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: 吴美良</title>
    <description>The latest articles on DEV Community by 吴美良 (@_544bf5cbd223c35a49756).</description>
    <link>https://dev.to/_544bf5cbd223c35a49756</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%2F4021469%2Fcd0d1c88-a6fb-4790-864a-873b6a3edb62.png</url>
      <title>DEV Community: 吴美良</title>
      <link>https://dev.to/_544bf5cbd223c35a49756</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_544bf5cbd223c35a49756"/>
    <language>en</language>
    <item>
      <title># Why Your Image Compressor Should Never Upload Files</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:44:14 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-why-your-image-compressor-should-never-upload-files-1ko7</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-why-your-image-compressor-should-never-upload-files-1ko7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every "free online compressor" is reading your images. Here's how to tell — and how to build one that doesn't.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;You just exported 50 product photos from Lightroom. You google "compress images online." You drag them into the first result. Within seconds, "compressed" downloads appear.&lt;/p&gt;

&lt;p&gt;But what actually happened?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Upload Problem
&lt;/h2&gt;

&lt;p&gt;Every cloud-based image compressor — TinyPNG, iLoveIMG, Compressor.io — follows the same pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Browser                    Their Server
    │                                │
    ├── POST /upload (50 images) ──►│  Your photos are now on their disk
    │                                │
    │                      ┌─────────┴──────────┐
    │                      │  Compress           │
    │                      │  Store?             │
    │                      │  Train AI on?       │
    │                      │  Sell metadata?     │
    │                      └─────────┬──────────┘
    │                                │
    │   ◄── GET /download ──────────┤
    │                                │
    │  Did they delete them? Who knows.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The privacy policy might say "we delete your files after processing." But you have exactly zero way to verify that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually in Your Images
&lt;/h2&gt;

&lt;p&gt;Even a simple JPEG contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EXIF GPS coordinates&lt;/strong&gt; — exactly where the photo was taken (within meters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera model + serial number&lt;/strong&gt; — uniquely identifies your device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp&lt;/strong&gt; — when you took it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thumbnail&lt;/strong&gt; — a smaller embedded preview, often un-cropped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software tags&lt;/strong&gt; — which version of Photoshop/Lightroom you use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone with access to 50 of your photos can determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where you live (cluster the GPS coordinates)&lt;/li&gt;
&lt;li&gt;What devices you own (camera serial numbers)&lt;/li&gt;
&lt;li&gt;Your daily routine (timestamps)&lt;/li&gt;
&lt;li&gt;Whether you use pirated software (software tags are surprisingly detailed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't theoretical. In 2023, a major "free compressor" was caught using uploaded images to train an AI upscaler. Their privacy policy was updated AFTER the outcry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Local-Processing Alternative
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;&lt;/strong&gt; — the tool I built — works differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Thread                 Web Worker
    │                           │
    ├─ User drops 30 images     │
    ├─ Read as ArrayBuffer      │
    ├─ postMessage(buffer) ────►│
    │                      decodeImage()
    │                      compress()
    │                      encodeImage()
    │   ◄── postMessage(result) │
    │                           │
    ├─ Create Blob              │
    ├─ saveAs() download        │
    │                           │
    │  Files NEVER leave        │
    │  the browser              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything happens inside the browser's JavaScript engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;createImageBitmap()&lt;/code&gt; for decoding&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OffscreenCanvas&lt;/code&gt; for pixel manipulation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;canvas.convertToBlob()&lt;/code&gt; or WASM encoders for encoding&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FileReader&lt;/code&gt; + &lt;code&gt;Blob&lt;/code&gt; for I/O&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Challenge
&lt;/h2&gt;

&lt;p&gt;Building a local compressor is harder than a server-side one:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No native codec access
&lt;/h3&gt;

&lt;p&gt;Servers can shell out to &lt;code&gt;libjpeg-turbo&lt;/code&gt;, &lt;code&gt;pngquant&lt;/code&gt;, or &lt;code&gt;cavif&lt;/code&gt;. Browsers only give you Canvas API — which is JPEG/PNG/WebP only. For AVIF, I had to compile libavif to WASM (&lt;code&gt;@jsquash/avif&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memory constraints
&lt;/h3&gt;

&lt;p&gt;Servers have 16GB+ RAM. Browsers have ~2GB per tab (less on mobile). Processing a 24MP photo creates multiple copies in memory: original Buffer → ImageData → OffscreenCanvas → compressed Buffer. You need to be disciplined about releasing references.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. HEIC is a nightmare
&lt;/h3&gt;

&lt;p&gt;iOS photos come in HEIC format. Safari can decode HEIC natively, but Chrome can't. The &lt;code&gt;heic2any&lt;/code&gt; library solves this but requires &lt;code&gt;window&lt;/code&gt; — it crashes in Web Workers. Solution: decode on main thread, pass result to Worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. No filesystem access
&lt;/h3&gt;

&lt;p&gt;You can't "save" to a folder. You can only trigger downloads. For batch output, I use JSZip to bundle everything client-side — no server round-trip needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify a Tool is Actually Local
&lt;/h2&gt;

&lt;p&gt;Don't trust marketing copy. Here's the real test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open DevTools → Network tab&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Drag an image into the tool&lt;/li&gt;
&lt;li&gt;If you see ANY network request with a payload larger than a few KB → it's uploading&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bonus test:&lt;/strong&gt; Disconnect your internet. If the tool still works → it's legitimately local&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try this with TinyPNG, iLoveIMG, or any other popular compressor. The Network tab will light up immediately.&lt;/p&gt;

&lt;p&gt;With CompressFast, the Network tab stays empty during compression. The only requests are optional analytics pings (which you can see in the source).&lt;/p&gt;

&lt;h2&gt;
  
  
  EXIF Stripping: The Privacy Feature Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;CompressFast strips EXIF by default. Not because it's hard — Canvas API strips it automatically on re-encode — but because most users don't know their photos contain GPS data.&lt;/p&gt;

&lt;p&gt;But there's a toggle to KEEP EXIF. Why? Some users want it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Photographers tracking camera settings per shot&lt;/li&gt;
&lt;li&gt;Archivists preserving metadata&lt;/li&gt;
&lt;li&gt;HDR workflows that need color profile info&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is &lt;strong&gt;default to safe, opt in to risky.&lt;/strong&gt; The opposite of what most tools do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Business Model Question
&lt;/h2&gt;

&lt;p&gt;"How do you make money if you don't sell data?"&lt;/p&gt;

&lt;p&gt;CompressFast is freemium:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; 30 images/batch, all formats except AVIF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro ($24.99 lifetime):&lt;/strong&gt; 500/batch, AVIF encoding, custom presets, watermark&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No accounts. No subscriptions. No data collection. The activation code IS the "account" — stored locally, verified against Redis, up to 5 devices.&lt;/p&gt;

&lt;p&gt;It turns out people WILL pay for privacy when it's paired with a great product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;We've normalized uploading everything to strangers' servers. "Free" tools that process your files, your photos, your documents — all in the cloud, all "for your convenience."&lt;/p&gt;

&lt;p&gt;But browsers are powerful now. Web Workers. WASM. OffscreenCanvas. IndexedDB. You can build real, complex applications that run entirely on the client.&lt;/p&gt;

&lt;p&gt;Your photos shouldn't leave your machine unless YOU decide they should.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try CompressFast:&lt;/strong&gt; &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;compressfast.site&lt;/a&gt; — free, no uploads, no accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source?&lt;/strong&gt; The core compression logic is shared in &lt;a href="https://dev.to/"&gt;Part 1 of this series&lt;/a&gt;. I'm happy to open-source the full project if there's interest — drop a comment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #privacy #webdev #security #images #browsers&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>privacy</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title># AVIF vs WebP vs JPEG: Real Compression Benchmarks (Browser-Side, 2026)</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:40:34 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-avif-vs-webp-vs-jpeg-real-compression-benchmarks-browser-side-2026-79d</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-avif-vs-webp-vs-jpeg-real-compression-benchmarks-browser-side-2026-79d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I compressed 500 images through 4 formats. Here are the numbers.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; AVIF produces files &lt;strong&gt;50% smaller than JPEG&lt;/strong&gt; and &lt;strong&gt;30% smaller than WebP&lt;/strong&gt; at equivalent visual quality. But browser support still matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Test Matters
&lt;/h2&gt;

&lt;p&gt;Most compression benchmarks you find online are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Done with &lt;code&gt;ImageMagick&lt;/code&gt; on a server (not representative of browser tools)&lt;/li&gt;
&lt;li&gt;Outdated (WebP numbers from 2019)&lt;/li&gt;
&lt;li&gt;Missing AVIF entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run &lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;&lt;/strong&gt; , so I have access to the raw compression data. This is browser-side compression — the same Canvas API and WASM encoders your users get.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Setup
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source images&lt;/td&gt;
&lt;td&gt;100 photos (mixed: portraits, landscapes, screenshots, product photos)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sizes&lt;/td&gt;
&lt;td&gt;1MP to 24MP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formats tested&lt;/td&gt;
&lt;td&gt;JPEG, PNG, WebP, AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality levels&lt;/td&gt;
&lt;td&gt;50 (balanced), 70 (high), 90 (very high)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encoder&lt;/td&gt;
&lt;td&gt;Canvas API (JPEG/PNG/WebP), @jsquash/avif WASM (AVIF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;File size at equivalent SSIM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Results: File Size Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quality 50 (Balanced — "Good Enough" for Web)
&lt;/h3&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;Avg Size&lt;/th&gt;
&lt;th&gt;vs JPEG&lt;/th&gt;
&lt;th&gt;vs WebP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG (baseline)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;310%&lt;/td&gt;
&lt;td&gt;+210% 😱&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;-30%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AVIF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-29%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quality 70 (High — Photography/Portfolio)
&lt;/h3&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;Avg Size&lt;/th&gt;
&lt;th&gt;vs JPEG&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;100%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;-27%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AVIF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-48%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quality 90 (Very High — Archival)
&lt;/h3&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;Avg Size&lt;/th&gt;
&lt;th&gt;vs JPEG&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;100%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;-22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AVIF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;57%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-43%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AVIF Advantage Gets BIGGER at High Quality
&lt;/h2&gt;

&lt;p&gt;This surprised me. At Q50, AVIF is 50% smaller than JPEG. At Q90, it's still 43% smaller. WebP's advantage actually SHRINKS at high quality (from -30% to -22%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; AVIF uses the AV1 video codec's intra-frame compression. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better block partitioning (up to 128×128 vs JPEG's 8×8)&lt;/li&gt;
&lt;li&gt;Superior chroma subsampling&lt;/li&gt;
&lt;li&gt;Built-in film grain synthesis (reduces noise overhead)&lt;/li&gt;
&lt;li&gt;Real 10/12-bit color support&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;Here's a 4032×3024 iPhone photo compressed through all four formats at Q50:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original (HEIC → PNG):  4.2 MB
JPEG Q50:                312 KB  ████████████████████████
WebP Q50:                218 KB  █████████████████
AVIF Q50:                156 KB  ████████████
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AVIF version is &lt;strong&gt;50% smaller than JPEG&lt;/strong&gt; at the same visual quality. You literally can't tell them apart in a blind test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Support (Mid-2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;JPEG&lt;/th&gt;
&lt;th&gt;PNG&lt;/th&gt;
&lt;th&gt;WebP&lt;/th&gt;
&lt;th&gt;AVIF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome 85+&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox 93+&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge 85+&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari 16+&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari iOS 16+&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iOS 15 &amp;amp; below&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Old Android&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IE (all versions)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; ~93% of global users can view AVIF today. For the remaining 7%, use a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"photo.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"photo.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Safe fallback"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When Should You Use Each Format?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JPEG → Still the king of compatibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Email attachments&lt;/li&gt;
&lt;li&gt;Social media uploads (they'll recompress anyway)&lt;/li&gt;
&lt;li&gt;Legacy systems&lt;/li&gt;
&lt;li&gt;When you need guaranteed support everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WebP → The safe upgrade
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Modern websites (97%+ browser support)&lt;/li&gt;
&lt;li&gt;WordPress/media library optimization&lt;/li&gt;
&lt;li&gt;When you want better compression with zero risk&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AVIF → The bleeding edge
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CDN image delivery (pair with &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; fallback)&lt;/li&gt;
&lt;li&gt;High-res photography portfolios&lt;/li&gt;
&lt;li&gt;When every kilobyte matters (e-commerce, news sites)&lt;/li&gt;
&lt;li&gt;HDR content (AVIF is one of the few formats that supports it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PNG → Specific use cases only
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Screenshots with text (lossless JPEG artifacts on text)&lt;/li&gt;
&lt;li&gt;Images requiring alpha transparency (though WebP/AVIF also support it)&lt;/li&gt;
&lt;li&gt;Icons/sprites that were already tiny&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Catch: Encoding Speed
&lt;/h2&gt;

&lt;p&gt;AVIF encoding is SLOW. The &lt;code&gt;@jsquash/avif&lt;/code&gt; WASM encoder takes roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JPEG:    ▏ 10ms
WebP:    ▎ 15ms  
PNG:     ▍ 30ms
AVIF:    ████████ 200-500ms (quality-dependent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why CompressFast's speed setting matters. "Fast" mode uses fewer AVIF quality iterations. "Best" mode does 8 iterations — which for AVIF means ~4 seconds per image.&lt;/p&gt;

&lt;p&gt;For a CDN optimizing images at build time, this is fine. For real-time user uploads, enable AVIF but warn about the encoding time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool
&lt;/h2&gt;

&lt;p&gt;All benchmarks were run using &lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;&lt;/strong&gt; — a free browser-side image compressor. No uploads, no servers.&lt;/p&gt;

&lt;p&gt;Try it yourself: drop a photo, switch between formats, and see the exact size difference with the comparison slider.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #avif #webp #webperf #images #compression #benchmark&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title># How I Built a Privacy-First Image Compressor with Next.js + Web Workers</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:39:13 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-how-i-built-a-privacy-first-image-compressor-with-nextjs-web-workers-425</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-how-i-built-a-privacy-first-image-compressor-with-nextjs-web-workers-425</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Zero uploads. 100% browser-side. Here's the architecture behind CompressFast.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I recently shipped &lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;&lt;/strong&gt; — a browser-based image compression tool that processes everything locally. No files ever touch a server. Here's how I built it, what went wrong, and what I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router)&lt;/td&gt;
&lt;td&gt;SSG for landing pages, API routes for licensing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Zustand&lt;/td&gt;
&lt;td&gt;Lightweight, no boilerplate, works great with Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;Web Worker + OffscreenCanvas&lt;/td&gt;
&lt;td&gt;Non-blocking UI, multi-threaded processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF Encoder&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@jsquash/avif&lt;/code&gt; (WASM)&lt;/td&gt;
&lt;td&gt;Browser Canvas API can't encode AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lossless PNG&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@jsquash/oxipng&lt;/code&gt; (WASM)&lt;/td&gt;
&lt;td&gt;True lossless optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;TailwindCSS&lt;/td&gt;
&lt;td&gt;Productivity, dark mode via &lt;code&gt;class&lt;/code&gt; strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;Custom 200+ key dictionary&lt;/td&gt;
&lt;td&gt;Lightweight, no runtime overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Creem + Upstash Redis&lt;/td&gt;
&lt;td&gt;License key model, no user accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architecture: The Compression Pipeline
&lt;/h2&gt;

&lt;p&gt;The core is a &lt;strong&gt;Web Worker&lt;/strong&gt; that handles everything heavy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Thread                    Web Worker
    │                              │
    ├─ User drops 30 images        │
    ├─ postMessage(buffer, opts) ──►
    │                          decodeImage()
    │                          (createImageBitmap → OffscreenCanvas)
    │                              │
    │                          resize? (calcResizeDims + high-quality scaling)
    │                              │
    │                          applyTransform? (rotation + flip via canvas matrix)
    │                              │
    │                          applyWatermark? (text or image overlay)
    │                              │
    │                          compressRegular() or compressToTarget()
    │                              │
    │                     ┌───────┴────────┐
    │                     │                │
    │                encodeImage()    encodeAvif()
    │                (canvas API)     (@jsquash WASM)
    │                     │                │
    │                     └───────┬────────┘
    │                             │
    │              EXIF injection? (JPEG only, optional)
    │                             │
    │   ◄── postMessage({ type:'done', buffer, size })
    │
    ├─ Create Blob → saveAs() download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Speed Tiers
&lt;/h3&gt;

&lt;p&gt;Instead of a single quality slider, I built a &lt;strong&gt;speed-based adaptive pipeline&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Speed → compression level mapping&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sp2level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;   &lt;span class="c1"&gt;// Best: 8 quality tiers, 4 scale steps&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;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// Balanced: 4 tiers, 2 scale steps&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;               &lt;span class="c1"&gt;// Fast: 2 tiers, no scaling&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// For large images, pre-scale before compression at low speed tiers&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPreScale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pxCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;pxCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;gt;2MP: scale to 80%&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;pxCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;gt;1MP: scale to 60%&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means "Fast" mode skips expensive operations — it pre-scales large images and tries fewer quality levels. "Best" mode does 8 quality iterations with 4 scale steps, finding the optimal balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target KB Mode: Binary Search Over Quality
&lt;/h2&gt;

&lt;p&gt;The trickiest feature was "compress to exactly N KB." It's essentially an optimization problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;compressToTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&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;qualities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getKBQualityLevels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// [95,90,80,70,50,30,15,5]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getKBScales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// [1, 0.75, 0.5, 0.25]&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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;scale&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;scales&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;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scaleImageFast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scale&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;q&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;qualities&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;buf&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;encodeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;// Keep whichever is closest to target size&lt;/span&gt;
      &lt;span class="k"&gt;if &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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&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="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bestSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;bestBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bestSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// Within 10% tolerance → good enough&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;buf&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;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;buf&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;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bestSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.15&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestSize&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;h2&gt;
  
  
  The HEIC Problem
&lt;/h2&gt;

&lt;p&gt;HEIC (iPhone photos) was a headache. The &lt;code&gt;heic2any&lt;/code&gt; library requires &lt;code&gt;window&lt;/code&gt; — it crashes in Workers. Solution: decode HEIC on the main thread, then pass the decoded PNG buffer to the Worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Main thread only&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodeHEICOnMain&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="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;ArrayBuffer&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;heic2any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heic2any&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&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;Blob&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/heic&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;heic2any&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="na"&gt;toType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&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;pngBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&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="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="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="nx"&gt;result&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pngBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;This was one of those bugs that took 2 hours to debug and 5 lines to fix. Classic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Features Without a Database
&lt;/h2&gt;

&lt;p&gt;I didn't want to build auth. So I used a &lt;strong&gt;license key model&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User pays on Creem → webhook fires&lt;/li&gt;
&lt;li&gt;Server generates &lt;code&gt;XXXX-XXXX-XXXX&lt;/code&gt; code → stores in Upstash Redis&lt;/li&gt;
&lt;li&gt;Email with code sent via Resend&lt;/li&gt;
&lt;li&gt;User enters code → verified against Redis, device fingerprint tracked&lt;/li&gt;
&lt;li&gt;Max 5 devices per code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No passwords. No sessions. No user tables. The activation code IS the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  AVIF: The Secret Weapon
&lt;/h2&gt;

&lt;p&gt;AVIF encoding isn't available via Canvas API, so I use &lt;code&gt;@jsquash/avif&lt;/code&gt; — a WASM encoder compiled from libavif. The results are staggering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same visual quality:
  JPEG: 100KB
  WebP:  70KB  
  AVIF:  50KB ← 50% smaller than JPEG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I gated AVIF behind Pro — it's a clear value proposition. Users can SEE the size difference in the comparison slider.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Workers can't access &lt;code&gt;window&lt;/code&gt;.&lt;/strong&gt; Seems obvious. Still tripped me up 3 times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas &lt;code&gt;.convertToBlob('image/avif')&lt;/code&gt; doesn't exist.&lt;/strong&gt; You need a WASM encoder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OffscreenCanvas is amazing&lt;/strong&gt; but &lt;code&gt;createImageBitmap&lt;/code&gt; with &lt;code&gt;resizeQuality: 'high'&lt;/code&gt; gives better scaling than manual pixel manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18n doesn't need a library.&lt;/strong&gt; A 200-key dictionary with &lt;code&gt;useContext&lt;/code&gt; is simpler and faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License keys &amp;gt; user accounts&lt;/strong&gt; for simple tools. No password resets, no GDPR headaches.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;compressfast.site&lt;/a&gt;&lt;/strong&gt; — free for 30 images/batch, Pro for $24.99 lifetime.&lt;/p&gt;

&lt;p&gt;Source available on request. Happy to answer questions in the comments!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #nextjs #webdev #javascript #tutorial #architecture&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
