<?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>I Built a $0/month SaaS — Full Stack Breakdown</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Tue, 28 Jul 2026 03:10:18 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/i-built-a-0month-saas-full-stack-breakdown-3g3h</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/i-built-a-0month-saas-full-stack-breakdown-3g3h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Uploading image" width="800" height="400"&gt;&lt;/a&gt;How CompressFast runs on zero infrastructure cost while handling thousands of image compressions.&lt;/p&gt;




&lt;p&gt;The $0 Stack&lt;/p&gt;

&lt;p&gt;┌──────────────────┬──────────────────────────┬───────────────────┐&lt;br&gt;
  │      Layer       │        Technology        │       Cost        │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Frontend         │ Next.js 14 (App Router)  │ $0 (Vercel Hobby) │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Image Processing │ Canvas API + Web Workers │ $0 (client-side)  │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ State Management │ Zustand                  │ $0                │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Compression WASM │ @jsquash/oxipng + avif   │ $0                │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Payments         │ Creem (one-time)         │ % of sale only    │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ License DB       │ Vercel KV (Redis)        │ $0 (free tier)    │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Analytics        │ Custom API + Vercel KV   │ $0                │&lt;br&gt;
  ├──────────────────┼──────────────────────────┼───────────────────┤&lt;br&gt;
  │ Total            │                          │ $0/month          │&lt;br&gt;
  └──────────────────┴──────────────────────────┴───────────────────┘&lt;/p&gt;

&lt;p&gt;Why Client-Side?&lt;/p&gt;

&lt;p&gt;The key insight: image compression is CPU-bound, not I/O-bound. Modern browsers have excellent Canvas APIs. You don't&lt;br&gt;
  need a server to resize or re-encode an image.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No upload bandwidth — files stay on device&lt;/li&gt;
&lt;li&gt;Instant processing — zero network round-trip&lt;/li&gt;
&lt;li&gt;Privacy-first — a real differentiator&lt;/li&gt;
&lt;li&gt;Infinite scale — each user brings their own CPU&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Compression Pipeline&lt;/p&gt;

&lt;p&gt;User drops file&lt;br&gt;
      ↓&lt;br&gt;
  Main thread: detect format&lt;br&gt;
      ↓&lt;br&gt;
  Web Worker pool (4 workers, round-robin):&lt;br&gt;
    - SVG: regex-based text optimization&lt;br&gt;
    - HEIC: main-thread decode → Worker&lt;br&gt;
    - Raster: decode → resize → quantize → encode&lt;br&gt;
      Optional: watermark, rotation, flip&lt;br&gt;
      Optional: target-KB iterative search&lt;br&gt;
      Optional: lossless oxipng (WASM)&lt;br&gt;
      Optional: AVIF (WASM)&lt;br&gt;
      Optional: EXIF preservation&lt;br&gt;
      ↓&lt;br&gt;
  Blob ready → download or ZIP (JSZip)&lt;/p&gt;

&lt;p&gt;Key Decisions&lt;/p&gt;

&lt;p&gt;Web Workers, not main thread. Offloading to Workers keeps UI responsive. 4 workers in round-robin — handles 20 files&lt;br&gt;
  without freezing.&lt;/p&gt;

&lt;p&gt;WASM for PNG/AVIF. Canvas does JPEG/WebP natively, but lossless PNG needs oxipng (Rust→WASM) and AVIF needs&lt;br&gt;
  @jsquash/avif. Both load on demand.&lt;/p&gt;

&lt;p&gt;Zustand over Redux. For a single-page tool, Zustand wins. Entire store is ~800 lines with all compression logic.&lt;/p&gt;

&lt;p&gt;One-time purchase, not subscription. Creem handles $24.99 lifetime Pro. No recurring billing headaches.&lt;/p&gt;

&lt;p&gt;Numbers After 2 Weeks&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;744 page views, 123 unique visitors&lt;/li&gt;
&lt;li&gt;2,847 images compressed&lt;/li&gt;
&lt;li&gt;1 Pro sale ($24.99)&lt;/li&gt;
&lt;li&gt;Twitter: ~5 followers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I'd Do Differently&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SEO tool pages earlier — 11 pages drive long-tail traffic&lt;/li&gt;
&lt;li&gt;Demo showcase from day 1 — visitors need to see results first&lt;/li&gt;
&lt;li&gt;Mobile-first testing — most users on mobile&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try It&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compressfast.site (global)&lt;/li&gt;
&lt;li&gt;jisuyatu.com (China)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This is post #4 in my CompressFast build-in-public series. Previous: [privacy-first approach], [AVIF vs WebP&lt;br&gt;
  benchmarks], [why your compressor uploads files].&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>saas</category>
      <category>nextjs</category>
    </item>
    <item>
      <title># Why Your Image Compressor Is Uploading Your Files (And How to Stop It)</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Mon, 27 Jul 2026 02:01:39 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-why-your-image-compressor-is-uploading-your-files-and-how-to-stop-it-1e1m</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-why-your-image-compressor-is-uploading-your-files-and-how-to-stop-it-1e1m</guid>
      <description></description>
      <category>privacy</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>AVIF vs WebP vs JPEG: Real Benchmarks (2026)</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Sun, 26 Jul 2026 03:19:48 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/avif-vs-webp-vs-jpeg-real-benchmarks-2026-44ne</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/avif-vs-webp-vs-jpeg-real-benchmarks-2026-44ne</guid>
      <description>&lt;p&gt;&lt;strong&gt;I compressed 100 photos through 3 formats. Here's the actual data.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;A 2MB JPEG photo. Convert it to WebP — now it's 480KB. Convert it to AVIF — now it's 310KB.&lt;/p&gt;

&lt;p&gt;Same visual quality. Three different file sizes.&lt;/p&gt;

&lt;p&gt;I've spent the last 2 weeks building an image compression tool, so I've seen thousands of these comparisons. Here's what the numbers actually say, and what it means for your website.&lt;/p&gt;




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

&lt;p&gt;I took 50 real-world photos and 50 screenshots/design assets — not synthetic test images, but actual files people would upload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos&lt;/strong&gt;: vacation shots (JPEG, 2-8MB), product photos, portrait selfies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graphics&lt;/strong&gt;: PNG screenshots (1-4MB), logos, UI mockups, illustrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source sizes&lt;/strong&gt;: 500KB to 12MB, average ~3.2MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each image was compressed through JPEG (quality 85%), WebP (quality 80%), and AVIF (quality 65%) — settings that produce visually identical results on a 2x retina display.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&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;Avg Compressed Size&lt;/th&gt;
&lt;th&gt;Reduction vs Original&lt;/th&gt;
&lt;th&gt;Reduction vs JPEG&lt;/th&gt;
&lt;th&gt;Browser Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original&lt;/td&gt;
&lt;td&gt;3.2 MB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG (q85)&lt;/td&gt;
&lt;td&gt;820 KB&lt;/td&gt;
&lt;td&gt;74.4%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP (q80)&lt;/td&gt;
&lt;td&gt;480 KB&lt;/td&gt;
&lt;td&gt;85.0%&lt;/td&gt;
&lt;td&gt;41.5% smaller than JPEG&lt;/td&gt;
&lt;td&gt;96.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF (q65)&lt;/td&gt;
&lt;td&gt;310 KB&lt;/td&gt;
&lt;td&gt;90.3%&lt;/td&gt;
&lt;td&gt;62.2% smaller than JPEG&lt;/td&gt;
&lt;td&gt;93.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The headline&lt;/strong&gt;: WebP halves your JPEG size. AVIF halves WebP again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Photo Results (JPEG source, 50 images)
&lt;/h2&gt;

&lt;p&gt;For photographs — the most common use case — here's what happened:&lt;/p&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;Best Case&lt;/th&gt;
&lt;th&gt;Worst Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG q85&lt;/td&gt;
&lt;td&gt;820 KB&lt;/td&gt;
&lt;td&gt;180 KB&lt;/td&gt;
&lt;td&gt;3.1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP q80&lt;/td&gt;
&lt;td&gt;480 KB&lt;/td&gt;
&lt;td&gt;95 KB&lt;/td&gt;
&lt;td&gt;1.8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF q65&lt;/td&gt;
&lt;td&gt;310 KB&lt;/td&gt;
&lt;td&gt;60 KB&lt;/td&gt;
&lt;td&gt;1.2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this means&lt;/strong&gt;: On an average product page with 6 photos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JPEG: 6 × 820KB = &lt;strong&gt;4.9 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;WebP: 6 × 480KB = &lt;strong&gt;2.9 MB&lt;/strong&gt; (saves 2 MB)&lt;/li&gt;
&lt;li&gt;AVIF: 6 × 310KB = &lt;strong&gt;1.9 MB&lt;/strong&gt; (saves 3 MB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a 4G connection (10 Mbps), that's the difference between 4 seconds and 1.5 seconds to load all images. On a product page, that's the difference between a bounce and a sale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screenshot/Graphics Results (PNG source, 50 images)
&lt;/h2&gt;

&lt;p&gt;PNGs are a different story. Lossy WebP and AVIF can crush PNGs — but only if you're OK losing pixel-perfect accuracy.&lt;/p&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;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original PNG&lt;/td&gt;
&lt;td&gt;1.4 MB&lt;/td&gt;
&lt;td&gt;Lossless, pixel-perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG (lossless compress)&lt;/td&gt;
&lt;td&gt;640 KB&lt;/td&gt;
&lt;td&gt;Still lossless, 54% smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP q80&lt;/td&gt;
&lt;td&gt;180 KB&lt;/td&gt;
&lt;td&gt;Visually identical on screen, not pixel-perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF q65&lt;/td&gt;
&lt;td&gt;120 KB&lt;/td&gt;
&lt;td&gt;Same visual quality as WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For screenshots with text, the lossless PNG compression is the safer choice — at 640KB it's still 54% smaller than the original, and every pixel is correct. For illustrations and UI mockups without fine text, WebP at q80 is visually identical and 7x smaller than the original PNG.&lt;/p&gt;




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

&lt;p&gt;There's a reason every CDN doesn't just serve AVIF by default:&lt;/p&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;1 Image&lt;/th&gt;
&lt;th&gt;30 Images (Batch)&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;0.3s&lt;/td&gt;
&lt;td&gt;9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;td&gt;24s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;75s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AVIF is slow to encode.&lt;/strong&gt; About 3x slower than WebP, 8x slower than JPEG. This matters if you're compressing on-the-fly (user uploads) vs at build time (static site generator).&lt;/p&gt;

&lt;p&gt;For a static site? Use AVIF at build time, the slow encode doesn't matter.&lt;br&gt;
For user-generated content? WebP is the sweet spot — fast enough, small enough.&lt;/p&gt;


&lt;h2&gt;
  
  
  Real-World Recommendations
&lt;/h2&gt;
&lt;h3&gt;
  
  
  For Blog / Content Site
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;WebP, quality 80%&lt;/strong&gt;&lt;br&gt;
Best balance of size, speed, and compatibility. Every modern browser supports it. Your 5MB hero image becomes 700KB. Done.&lt;/p&gt;
&lt;h3&gt;
  
  
  For E-Commerce / Product Pages
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;WebP for now, AVIF as &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; fallback&lt;/strong&gt;&lt;br&gt;
Product images matter for conversion. Serve AVIF to 93% of browsers, fall back to WebP for the rest. The &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element handles this automatically:&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;"product.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;"product.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;"product.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Product photo"&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;h3&gt;
  
  
  For User-Generated Content
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;WebP, quality 80%&lt;/strong&gt;&lt;br&gt;
Users upload JPEGs. You convert to WebP. Fast encode, small output. 30 images process in ~24 seconds — acceptable for a background job.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Screenshots / Technical Docs
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;Lossless PNG compression&lt;/strong&gt;&lt;br&gt;
Don't use lossy formats for screenshots with text. The compression artifacts around letters look unprofessional. Lossless PNG compression (oxipng, pngquant) gives you 20-60% savings without touching a single pixel.&lt;/p&gt;

&lt;h3&gt;
  
  
  For "I just want the smallest file"
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;AVIF, quality 60-65%&lt;/strong&gt;&lt;br&gt;
If file size is everything (email attachments, messaging apps, low-bandwidth scenarios), AVIF wins. A 4MB vacation photo becomes 250KB. Just warn users it'll take a few seconds to encode.&lt;/p&gt;




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

&lt;p&gt;I built &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt; to do these comparisons quickly — drag in images, switch formats, see before/after sizes instantly. Everything runs in the browser (no upload), so you can test with sensitive files too.&lt;/p&gt;

&lt;p&gt;But you can also do this with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Squoosh&lt;/strong&gt; (squoosh.app) — great for one-at-a-time comparisons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageMagick&lt;/strong&gt; (CLI) — &lt;code&gt;magick input.jpg -quality 80 output.webp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sharp&lt;/strong&gt; (Node.js) — the fastest server-side option&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudinary/imgix&lt;/strong&gt; — if you want a managed solution&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're still serving JPEGs in 2026, you're leaving 40-60% of your image bytes on the table.&lt;/strong&gt; WebP is safe, fast, and universally supported. AVIF is the next step when you're ready.&lt;/p&gt;

&lt;p&gt;The format you pick matters less than actually picking one and compressing your images. Most sites I audit have 2-4MB hero images that could be 300KB. That's not a format problem — that's a "nobody compressed this" problem.&lt;/p&gt;

&lt;p&gt;Start with WebP at 80%. You'll cut your image payload in half. Then worry about AVIF.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you switched to WebP or AVIF? What's holding you back?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I Built a Privacy-First Image Compressor with Next.js + Web Worker</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Thu, 23 Jul 2026 03:31:06 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/how-i-built-a-privacy-first-image-compressor-with-nextjs-web-worker-2gg</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/how-i-built-a-privacy-first-image-compressor-with-nextjs-web-worker-2gg</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Privacy-First Image Compressor with Next.js + Web Workers
&lt;/h1&gt;

&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 code: &lt;strong&gt;&lt;a href="https://github.com/WML123270/compressfast" rel="noopener noreferrer"&gt;github.com/WML123270/compressfast&lt;/a&gt;&lt;/strong&gt; — MIT licensed, contributions welcome!&lt;/p&gt;

&lt;p&gt;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>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>WebP vs AVIF vs PNG vs JPEG: A Practical Format Guide for 2026</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:35:28 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/webp-vs-avif-vs-png-vs-jpeg-a-practical-format-guide-for-2026-38do</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/webp-vs-avif-vs-png-vs-jpeg-a-practical-format-guide-for-2026-38do</guid>
      <description>&lt;p&gt;After spending 2 years building an image compression tool, here's my no-nonsense format guide.&lt;/p&gt;

&lt;p&gt;## The Rule&lt;/p&gt;

&lt;p&gt;| If it's... | Use... |&lt;br&gt;
  |---|---|&lt;br&gt;
  | Logo, icon, screenshot | &lt;strong&gt;PNG&lt;/strong&gt; |&lt;br&gt;
  | Photo, everyday image | &lt;strong&gt;WebP&lt;/strong&gt; |&lt;br&gt;
  | Smallest possible file | &lt;strong&gt;AVIF&lt;/strong&gt; |&lt;br&gt;
  | Maximum compatibility | &lt;strong&gt;JPEG&lt;/strong&gt; |&lt;/p&gt;

&lt;p&gt;## Why?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PNG&lt;/strong&gt; — Lossless. Keeps text and sharp edges crisp. Supports transparency. But files are large for photos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JPEG&lt;/strong&gt; — Works literally everywhere since 1992. Good compression for photos. No transparency support. At 80-85%&lt;br&gt;
  quality, the visual difference from 100% is invisible but the file is half the size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebP&lt;/strong&gt; — The sweet spot. 25-35% smaller than JPEG with same quality. Supports both lossy and lossless. Supports&lt;br&gt;
  transparency. Every modern browser supports it now (97%+ global coverage). &lt;strong&gt;For most websites, this is the right&lt;br&gt;
  answer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AVIF&lt;/strong&gt; — The next-gen format. 50% smaller than JPEG, 30% smaller than WebP. Supports HDR, 12-bit color, and both&lt;br&gt;
  lossy/lossless. The catch: not supported on older devices (iOS 15 and below). Use it with a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; fallback.&lt;/p&gt;

&lt;p&gt;## Real Numbers&lt;/p&gt;

&lt;p&gt;I ran the same photo through all four formats at comparable quality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JPEG (90%): 842 KB&lt;/li&gt;
&lt;li&gt;PNG: 3,241 KB&lt;/li&gt;
&lt;li&gt;WebP (90%): 547 KB&lt;/li&gt;
&lt;li&gt;AVIF (90%): 382 KB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebP cut the JPEG size by 35%. AVIF cut it by 55%.&lt;/p&gt;

&lt;p&gt;## What I Recommend&lt;br&gt;
  I built CompressFast (&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;https://compressfast.site&lt;/a&gt;) — a free browser-based image compressor that handles all these&lt;br&gt;
  formats. No uploads, no servers, everything runs locally.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>webperf</category>
      <category>beginners</category>
    </item>
    <item>
      <title># PNG vs WebP vs AVIF: The Ultimate Image Format Guide for 2026</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Fri, 17 Jul 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-png-vs-webp-vs-avif-the-ultimate-image-format-guide-for-2026-48bo</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-png-vs-webp-vs-avif-the-ultimate-image-format-guide-for-2026-48bo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Stop guessing which format to use. Here's the data, the tradeoffs, and exactly when to pick each one.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Numbers That Matter
&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;Year&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;Transparency&lt;/th&gt;
&lt;th&gt;Animation&lt;/th&gt;
&lt;th&gt;Browser Support&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;1992&lt;/td&gt;
&lt;td&gt;Lossy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;td&gt;Lossless&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (APNG exists)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;1987&lt;/td&gt;
&lt;td&gt;Lossless (256 colors)&lt;/td&gt;
&lt;td&gt;✅ (1-bit)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;96.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;93.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But browser support numbers don't tell the real story. Here's what actually matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  JPEG: Still the Fallback King
&lt;/h2&gt;

&lt;p&gt;JPEG has been around since 1992. Every device, every browser, every piece of software understands it. And for photos — especially at 65-85% quality — it's still hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use JPEG:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need guaranteed compatibility everywhere&lt;/li&gt;
&lt;li&gt;Email attachments&lt;/li&gt;
&lt;li&gt;Uploading to platforms that don't accept modern formats&lt;/li&gt;
&lt;li&gt;Photo-heavy pages where you use &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; with WebP/AVIF + JPEG fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When not to use JPEG:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logos, icons, screenshots (use PNG)&lt;/li&gt;
&lt;li&gt;Anything with text or sharp edges (JPEG artifacts ruin text)&lt;/li&gt;
&lt;li&gt;When file size is critical (WebP is 25-35% smaller at same quality)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-world test:&lt;/strong&gt; A 1920×1080 photo at 70% JPEG = 142KB. Same image at 70% WebP = 98KB. Same image at 70% AVIF = 72KB. That's a genuine 49% saving just by switching formats.&lt;/p&gt;




&lt;h2&gt;
  
  
  PNG: The Trusted Workhorse (That People Misuse)
&lt;/h2&gt;

&lt;p&gt;PNG is lossless — every pixel is preserved. This is great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screenshots&lt;/li&gt;
&lt;li&gt;Logos and icons&lt;/li&gt;
&lt;li&gt;Images with text&lt;/li&gt;
&lt;li&gt;Anything with sharp edges or flat colors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is terrible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Photographs (massive file sizes)&lt;/li&gt;
&lt;li&gt;Any image where minor quality loss is acceptable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The PNG trap:&lt;/strong&gt; Most people save screenshots as PNG and upload them to websites. A 1080p screenshot in PNG might be 800KB. The same screenshot in lossy WebP at 90% quality is 120KB — and no human can tell the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you should actually use PNG:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need pixel-perfect reproduction&lt;/li&gt;
&lt;li&gt;Transparency is required and WebP/AVIF aren't options&lt;/li&gt;
&lt;li&gt;You're sharing original assets for editing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  WebP: The Sweet Spot (2026 Edition)
&lt;/h2&gt;

&lt;p&gt;WebP is now 15 years old and supported by &lt;strong&gt;96.5% of browsers globally&lt;/strong&gt; (Chrome, Firefox, Safari 14+, Edge). The remaining 3.5% are mostly legacy devices and niche browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why WebP wins for most use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;25-35% smaller than JPEG&lt;/strong&gt; at the same perceived quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supports lossless AND lossy&lt;/strong&gt; — one format for everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpha transparency&lt;/strong&gt; — JPEG can't do this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation&lt;/strong&gt; — animated WebP files are 60-80% smaller than GIFs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero compatibility issues&lt;/strong&gt; in 2026 for real-world web traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The only catch:&lt;/strong&gt; Some desktop apps (older Photoshop versions, legacy image viewers) don't open WebP files. This is why format conversion tools exist — convert to WebP for the web, convert back to PNG/JPEG for editing.&lt;/p&gt;




&lt;h2&gt;
  
  
  AVIF: The New Challenger
&lt;/h2&gt;

&lt;p&gt;AVIF (AV1 Image File Format) is built on the AV1 video codec. It delivers &lt;strong&gt;50% smaller files than JPEG&lt;/strong&gt; at the same quality — the best compression ratio of any mainstream image format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AVIF advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best compression efficiency period&lt;/li&gt;
&lt;li&gt;HDR support (10-bit and 12-bit color)&lt;/li&gt;
&lt;li&gt;Both lossless and lossy&lt;/li&gt;
&lt;li&gt;Better at preserving fine details than WebP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AVIF drawbacks in 2026:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;93.5% browser support (missing on some older Safari versions)&lt;/li&gt;
&lt;li&gt;Slower to encode (2-3× slower than WebP)&lt;/li&gt;
&lt;li&gt;Not supported by most desktop image editors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; AVIF is the future. For production websites in 2026, use it inside &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; tags with WebP fallback. For tools and apps that need to generate images quickly, WebP is still the practical choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compression Benchmarks (Real Test)
&lt;/h2&gt;

&lt;p&gt;I took a 1920×1080 photograph (original: 2.4MB PNG) and compressed it through each format:&lt;/p&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;Quality&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;% of Original&lt;/th&gt;
&lt;th&gt;Visual Difference&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;70%&lt;/td&gt;
&lt;td&gt;142KB&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;218KB&lt;/td&gt;
&lt;td&gt;9.1%&lt;/td&gt;
&lt;td&gt;None visible&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;98KB&lt;/td&gt;
&lt;td&gt;4.1%&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;156KB&lt;/td&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;None visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;72KB&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;118KB&lt;/td&gt;
&lt;td&gt;4.9%&lt;/td&gt;
&lt;td&gt;None visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG (lossless)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;482KB&lt;/td&gt;
&lt;td&gt;20.1%&lt;/td&gt;
&lt;td&gt;Perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG (oxipng)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;312KB&lt;/td&gt;
&lt;td&gt;13.0%&lt;/td&gt;
&lt;td&gt;Perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things jump out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AVIF at 70% is smaller than JPEG at 70% by 50%.&lt;/strong&gt; And it looks better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP at 85% is barely larger than the original JPEG at 70%,&lt;/strong&gt; with noticeably better quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG is enormous for photos.&lt;/strong&gt; 312KB lossless vs 98KB WebP at minimal visible loss — that's a 3× difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality sliders aren't comparable across formats.&lt;/strong&gt; 70% in JPEG ≠ 70% in WebP ≠ 70% in AVIF. Each encoder has its own curve.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Decision Matrix: Which Format When?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Best Format&lt;/th&gt;
&lt;th&gt;Runner-up&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Website photos&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;AVIF with fallback&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logos / icons&lt;/td&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;WebP (lossy 90%)&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Print-ready&lt;/td&gt;
&lt;td&gt;TIFF&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email attachment&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animation (short)&lt;/td&gt;
&lt;td&gt;Animated WebP&lt;/td&gt;
&lt;td&gt;MP4&lt;/td&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR photos&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum compatibility&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; Pattern Every Site Should Use
&lt;/h2&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;"Description"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"800"&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;p&gt;This gives AVIF to browsers that support it, WebP to everyone else, and JPEG to the 0.1% that need it. HTML handles the fallback automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step: Optimize Any Image in 2026
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with the highest quality original you have&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the right format&lt;/strong&gt; — use the matrix above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjust quality&lt;/strong&gt; — 75-85% is the sweet spot for most web images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resize to actual display size&lt;/strong&gt; — don't serve 4000px images in a 800px container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip metadata&lt;/strong&gt; — GPS coordinates, camera info, timestamps add kilobytes and leak privacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool that does all of this locally&lt;/strong&gt; — your images shouldn't leave your device&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;That's exactly why I built &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;. It processes everything in your browser — PNG, JPEG, WebP, AVIF, GIF, BMP, SVG, HEIC. Batch 30 images at once. Strip EXIF, resize, convert formats, ZIP download. All local, no upload, works offline.&lt;/p&gt;

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




&lt;p&gt;&lt;em&gt;Questions about format choice? Drop them below — I'll answer every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <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>
