<?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>"How I Built a Browser Image Compressor That Handles 30 Images at Once"</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Sun, 16 Aug 2026 03:18:13 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/how-i-built-a-browser-image-compressor-that-handles-30-images-at-once-56k4</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/how-i-built-a-browser-image-compressor-that-handles-30-images-at-once-56k4</guid>
      <description>&lt;p&gt;Most image compressors do the actual work on a server. Mine runs entirely in the browser — 30 images at a time, re-encoded with the Canvas API, on your device. Here's the architecture that makes that possible, and the bugs that nearly broke it.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt; to scratch my own itch, and the constraint was non-negotiable: &lt;strong&gt;files must never leave the device&lt;/strong&gt;. That single decision shaped everything downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: keep the heavy work off the main thread
&lt;/h2&gt;

&lt;p&gt;Re-encoding a 4000×4000 photo is CPU-heavy. If you do it on the main thread, the UI freezes and the browser flags "page unresponsive". So the core is a &lt;strong&gt;Web Worker&lt;/strong&gt; that owns the entire compression pipeline:&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 (React)  ←→  Web Worker
   upload / UI              └─ decode → transform → encode → transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main thread handles selection, previews, and state. The worker does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decode&lt;/strong&gt; — &lt;code&gt;createImageBitmap&lt;/code&gt; for standard formats, a dedicated path for HEIC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transform&lt;/strong&gt; — resize, rotate, flip (applied &lt;em&gt;before&lt;/em&gt; encoding, never after)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encode&lt;/strong&gt; — &lt;code&gt;canvas.toBlob('image/webp', quality)&lt;/code&gt; (and JPEG/PNG/AVIF)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transfer&lt;/strong&gt; — the compressed &lt;code&gt;Blob&lt;/code&gt; is handed back via &lt;code&gt;postMessage&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key detail: use &lt;strong&gt;transferables&lt;/strong&gt; (&lt;code&gt;ArrayBuffer&lt;/code&gt;, not structured-clone of a Blob) wherever you can. For a 30-image batch, that's the difference between instant and a half-second of GC pressure per file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto format detection — the "smart" part
&lt;/h2&gt;

&lt;p&gt;Not every image should be compressed the same way. Photos want lossy WebP; screenshots and logos want lossless PNG. So the worker classifies each file and picks a strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG/photo&lt;/strong&gt; → WebP, quality 75–82% (the visual-equivalence sweet spot)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG/screenshot&lt;/strong&gt; → lossless (oxipng-style), 20–60% smaller with zero pixel change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GIF&lt;/strong&gt; → animated WebP (60–80% smaller)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEIC&lt;/strong&gt; (iPhone) → decoded first, then treated as a photo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why batch mode doesn't need per-file settings — the user drops a folder, and each image gets the right treatment automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bugs that took way too long to find
&lt;/h2&gt;

&lt;p&gt;If you're building anything with Web Workers, learn from my scars:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A worker can't reference &lt;code&gt;window&lt;/code&gt; — even transitively.&lt;/strong&gt;&lt;br&gt;
I pulled in a library whose module top-level touched &lt;code&gt;window&lt;/code&gt;. In the worker context, Webpack's chunk threw &lt;code&gt;ReferenceError: window is not defined&lt;/code&gt;, and every compression hung on the spinner with no error in the console. Fix: audit every worker import — &lt;code&gt;grep -r "window\|document" node_modules/&amp;lt;lib&amp;gt;&lt;/code&gt; — and keep browser-only libraries on the main thread, passing data to the worker afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Integer truncation broke image scaling.&lt;/strong&gt;&lt;br&gt;
My fast resize path used &lt;code&gt;Math.round(1/ratio)&lt;/code&gt; as a pixel step. For large images that truncation pushed the source coordinate past the row width, and the image came out split horizontally with black mosaic at the bottom. Fix: exact floating-point ratios plus a &lt;code&gt;Math.min()&lt;/code&gt; bounds guard on every manual pixel loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An async handler corrupted processing order.&lt;/strong&gt;&lt;br&gt;
I made the worker's message handler &lt;code&gt;await&lt;/code&gt; a transform after encoding. Async handlers don't preserve order — the rotation state got out of sync with the blob. Fix: keep worker message handlers synchronous. Apply transforms at download time, not inside the handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; / &lt;code&gt;Cross-Origin-Embedder-Policy&lt;/code&gt; headers will silently block worker scripts in production (they worked fine locally). Test any security header change against a real deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling up to 30 files without melting the tab
&lt;/h2&gt;

&lt;p&gt;Batch mode adds two more concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throttle re-renders.&lt;/strong&gt; Resize/compare previews fire on every pointer move. A &lt;code&gt;requestAnimationFrame&lt;/code&gt; throttle keeps it at 60fps instead of 300 wasted renders per second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; Blobs stay in memory until download. Packing the final results into a single ZIP (client-side, &lt;code&gt;jszip&lt;/code&gt;) keeps 30 "Download" buttons from becoming 30 separate saves.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A 1920×1080 screenshot goes 2.1 MB → 186 KB WebP (91%) → 102 KB AVIF (95%), all computed on-device with no server round-trip. Batch a whole folder, get one ZIP back.&lt;/p&gt;

&lt;p&gt;The whole stack — Next.js, a Web Worker, Canvas, and a ZIP library — costs $0/month to run, because there's no compression server to pay for. That's the real payoff of going browser-side: &lt;strong&gt;your users' machines do the work you'd otherwise pay for, and their files never leave their hands.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to try it: &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;compressfast.site&lt;/a&gt; — free, 30 files per batch, no account.&lt;/p&gt;

&lt;p&gt;What's the worst Web Worker bug you've hit? I'd love to add it to the list.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>"Image Optimization: The Complete 2026 Checklist for Faster Websites"</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Sat, 15 Aug 2026 04:00:28 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/image-optimization-the-complete-2026-checklist-for-faster-websites-3pi5</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/image-optimization-the-complete-2026-checklist-for-faster-websites-3pi5</guid>
      <description>&lt;p&gt;Images are the single biggest performance lever on almost every website. They average over 1MB of every 2MB page — and cutting them down is the fastest way to improve Core Web Vitals and SEO rankings.&lt;/p&gt;

&lt;p&gt;I've spent the past few months obsessing over image compression (I built CompressFast to scratch my own itch). Here's the complete, ordered checklist I use on every project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Resize first — it's the single biggest win
&lt;/h2&gt;

&lt;p&gt;A 6000×4000 photo served to a 1200px-wide card is pure waste. Before anything else, resize to the largest size your layout actually displays.&lt;/p&gt;

&lt;p&gt;Resizing alone cut my test image from 4.7MB to 1.1MB — a 77% reduction with zero quality change.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pick the right format per image type
&lt;/h2&gt;

&lt;p&gt;This is the mistake I see most often — using one format for everything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos&lt;/strong&gt; → WebP (30–50% smaller than JPEG) or AVIF (another 30% smaller)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots / UI&lt;/strong&gt; → PNG lossless, or WebP lossless&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logos / icons&lt;/strong&gt; → SVG (vector, tiny, infinitely scalable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animated GIFs&lt;/strong&gt; → animated WebP (60–80% smaller)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Compress, don't just convert
&lt;/h2&gt;

&lt;p&gt;Converting PNG → JPEG saves space, but then compressing that JPEG saves even more. Always run a compression pass &lt;em&gt;after&lt;/em&gt; conversion. Quality 75–82% is the sweet spot for photos — visually indistinguishable from 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Strip metadata
&lt;/h2&gt;

&lt;p&gt;EXIF, GPS coordinates, camera model, editing software tags — all useless for web display and can be 10–30KB per file. Strip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use lossless compression for text-heavy images
&lt;/h2&gt;

&lt;p&gt;For screenshots, logos, and anything with sharp edges, lossless tools (like oxipng for PNG) shrink files 20–60% &lt;em&gt;without changing a single pixel&lt;/em&gt;. Never run lossy compression on text — it creates ugly artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Lazy-load off-screen images
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;loading="lazy"&lt;/code&gt; (or &lt;code&gt;loading="lazy"&lt;/code&gt; via &lt;code&gt;next/image&lt;/code&gt;) stops below-the-fold images from blocking your LCP. Combine with &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; attributes to prevent layout shift (CLS).&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Serve modern formats with fallbacks
&lt;/h2&gt;

&lt;p&gt;Use the &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element to serve WebP/AVIF to modern browsers and JPEG as a 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;"..."&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;
  
  
  Real numbers from my testing
&lt;/h2&gt;

&lt;p&gt;A single 1920×1080 screenshot, compressed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;File size&lt;/th&gt;
&lt;th&gt;Reduction&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;2.1 MB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resize + WebP 75%&lt;/td&gt;
&lt;td&gt;186 KB&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resize + AVIF 50%&lt;/td&gt;
&lt;td&gt;102 KB&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 91–95% saving is the difference between a 4-second and a sub-1-second page load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy point most guides skip
&lt;/h2&gt;

&lt;p&gt;Most online compressors upload your files to &lt;em&gt;their&lt;/em&gt; server to do the compression. That means your (or your users') images are sitting on someone else's infrastructure. If you're compressing product shots, client work, or anything sensitive, use a tool that runs entirely in the browser — the file never leaves the device.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt; exactly for this: browser-side compression, no upload, no account, batch up to 30 files, free. It also auto-detects the right format per image so you don't have to think about steps 2–5.&lt;/p&gt;




&lt;p&gt;What's the biggest image optimization win you've found? Drop it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  webdev #performance #images
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title># I Built a Smart Image Compressor That Auto-Detects the Best Format</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Tue, 11 Aug 2026 04:54:48 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-i-built-a-smart-image-compressor-that-auto-detects-the-best-format-546e</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-i-built-a-smart-image-compressor-that-auto-detects-the-best-format-546e</guid>
      <description>&lt;p&gt;We've all been there — staring at a quality slider, wondering if 70% is too low or 90% is too high. Should this PNG stay as PNG? Should I convert to WebP? What even is lossless?&lt;/p&gt;

&lt;p&gt;I got tired of guessing, so I built &lt;strong&gt;Smart Compress&lt;/strong&gt; into &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt;. Drop any image, it figures out the optimal format and quality automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The smart analyzer looks at three things:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Format
&lt;/h3&gt;

&lt;p&gt;Each image type gets a different strategy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Smart Decision&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;PNG (small, likely logo/icon)&lt;/td&gt;
&lt;td&gt;Lossless PNG via oxipng&lt;/td&gt;
&lt;td&gt;Pixel-perfect, 20-60% smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG (large, likely photo)&lt;/td&gt;
&lt;td&gt;Convert to WebP 75%&lt;/td&gt;
&lt;td&gt;70%+ savings, still looks great&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG photo&lt;/td&gt;
&lt;td&gt;WebP 75%&lt;/td&gt;
&lt;td&gt;25-35% smaller, same quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG huge (&amp;gt;4MP)&lt;/td&gt;
&lt;td&gt;WebP 70% + resize to 1920px&lt;/td&gt;
&lt;td&gt;90%+ savings for web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;Animated WebP&lt;/td&gt;
&lt;td&gt;60-80% smaller, animation preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;Universal compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;Keep as-is&lt;/td&gt;
&lt;td&gt;Vector doesn't need compression&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Dimensions
&lt;/h3&gt;

&lt;p&gt;If an image is over 2 megapixels, Smart suggests resizing to 1920px wide. Most websites display images at 1200px max — those extra pixels are just wasted bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. File Size
&lt;/h3&gt;

&lt;p&gt;Larger files get more aggressive compression. A 10MB photo gets different treatment than a 200KB icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;The analysis engine is dead simple — just a function that takes file metadata and returns optimal settings:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;analyzeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FileMeta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SmartRecommendation&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;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLarge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pixels&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalSize&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHuge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000_000&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&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="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;isHuge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;resizeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pixels&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="mi"&gt;1920&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Large PNG → WebP (photo/screenshot, 70%+ savings)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lossless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PNG lossless (oxipng — pixel-perfect, 20-60% smaller)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JPEG → WebP 75% (25-35% smaller, same quality)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... more format handlers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;most images don't need custom settings&lt;/strong&gt;. A JPEG photo is a JPEG photo. The heuristics are simple because the use cases are predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI
&lt;/h2&gt;

&lt;p&gt;Instead of hiding this behind a settings panel, I put it front and center — a purple "Smart" button right next to the existing Max / Balanced / Best presets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│   📦     │ │   ⚖️     │ │   ✨     │ │   🧠 Smart   │
│   Max    │ │ Balanced │ │   Best   │ │   Auto       │
└──────────┘ └──────────┘ └──────────┘ └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clicking Smart shows a summary panel explaining exactly what it chose and why. No black box — users can see the reasoning and override if they want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: Everything Runs Locally
&lt;/h2&gt;

&lt;p&gt;Like everything in CompressFast, the Smart analysis runs &lt;strong&gt;entirely in the browser&lt;/strong&gt;. The file metadata (format, dimensions, size) is read locally — no data ever touches a server. You can unplug your internet after the page loads and Smart Compress still works.&lt;/p&gt;

&lt;p&gt;This is the whole philosophy of the project: compression is a solved problem, but most online tools upload your files to a server first. We do everything client-side with Web Workers and WebAssembly.&lt;/p&gt;

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

&lt;p&gt;Smart Compress is live now at &lt;strong&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;compressfast.site&lt;/a&gt;&lt;/strong&gt;. Free, no signup, no upload. Drop an image and click the purple Smart button.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're curious about the full tech stack (Next.js 14, Zustand, oxipng WASM, Creem payments — all for $0/month), I wrote about it &lt;a href="https://dev.to/_544bf5cbd223c35a49756/-i-built-a-0month-saas-full-stack-breakdown-4hpg"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a CLI Image Compressor — 73% Smaller in 0.1s</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:45:39 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/i-built-a-cli-image-compressor-73-smaller-in-01s-6pg</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/i-built-a-cli-image-compressor-73-smaller-in-01s-6pg</guid>
      <description>&lt;p&gt;I just shipped compressfast — a command-line image compressor that runs entirely on your machine. No uploads, no API&lt;br&gt;
  keys, no limits.&lt;/p&gt;

&lt;p&gt;One command:&lt;br&gt;
  npx compressfast photo.jpg --quality 80&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compress JPEG, PNG, WebP, AVIF&lt;/li&gt;
&lt;li&gt;Batch process entire folders&lt;/li&gt;
&lt;li&gt;Resize while compressing&lt;/li&gt;
&lt;li&gt;Watch mode for automatic processing&lt;/li&gt;
&lt;li&gt;All local — files never leave your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real results:&lt;br&gt;
  141.6 KB → 37.4 KB  (-73.6%)&lt;br&gt;
  Time: 0.1s&lt;/p&gt;

&lt;p&gt;Built with: Sharp (libvips) — the fastest image processing library for Node.js.&lt;/p&gt;

&lt;p&gt;npm: npmjs.com/package/compressfast&lt;br&gt;
  GitHub: github.com/WML123270/compressfast&lt;/p&gt;

&lt;p&gt;Try it: npx compressfast --help&lt;br&gt;
🔗 Try it: &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;https://compressfast.site&lt;/a&gt; — free, no upload, 8 formats supported.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title># I Built a $0/month SaaS — Full Stack Breakdown</title>
      <dc:creator>吴美良</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:49:15 +0000</pubDate>
      <link>https://dev.to/_544bf5cbd223c35a49756/-i-built-a-0month-saas-full-stack-breakdown-4hpg</link>
      <guid>https://dev.to/_544bf5cbd223c35a49756/-i-built-a-0month-saas-full-stack-breakdown-4hpg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;3,700+ images compressed. 178 users. 1 Pro sale. Total monthly hosting bill: $0.00.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built &lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;CompressFast&lt;/a&gt; — a privacy-first image compressor that runs entirely in the browser. No server-side processing. No uploads. No database costs.&lt;/p&gt;

&lt;p&gt;Here's exactly how the stack works, why it costs nothing, and what I'd change.&lt;/p&gt;




&lt;h2&gt;
  
  
  The $0 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;Service&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel (Hobby)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Upstash Redis (free tier)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend (100/day free)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Creem (5% per sale)&lt;/td&gt;
&lt;td&gt;$0 fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Vercel Analytics (free tier)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;compressfast.site&lt;/td&gt;
&lt;td&gt;~$3/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total monthly burn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only real cost is the domain renewal. Everything else rides on generous free tiers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Why There's No Server Bill
&lt;/h2&gt;

&lt;p&gt;The key insight: &lt;strong&gt;image compression doesn't need a server.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User's browser
    │
    ├─ Page loads (static HTML from Vercel CDN)
    ├─ User drops 30 images
    ├─ Web Workers spawn → Canvas API compresses locally
    ├─ Blob URLs generated → ZIP created client-side
    └─ Download starts instantly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No upload. No backend processing queue. No temporary storage. The browser does everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The only backend code is licensing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/create-license  → Generate activation code → Store in Redis
/api/verify-license  → Check code validity → Track device fingerprint
/api/creem           → Webhook: payment → issue license → email via Resend
/api/resend-license  → "Forgot your code?" → email it back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tiny API routes. Redis stores license keys + device IDs + stats. That's it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Worker Pool (0 Cost, 4x Speed)
&lt;/h2&gt;

&lt;p&gt;Instead of spawning/destroying workers per task, I keep a pool alive:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POOL_SIZE&lt;/span&gt; &lt;span class="o"&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;2&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;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&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;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;POOL_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Round-robin dispatch — no load balancer needed&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CompressTask&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="nx"&gt;CompressResult&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;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;taskIndex&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;POOL_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&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="nx"&gt;task&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="c1"&gt;// transferable = zero-copy&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;postMessage(buffer, [buffer])&lt;/code&gt; with transferable objects means zero-copy handoff. The buffer is literally moved, not cloned. 2x faster for large images.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compression Pipeline (All Client-Side)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│         Web Worker Pipeline          │
├─────────────────────────────────────┤
│ 1. decodeImage()                    │
│    createImageBitmap → OffscreenCanvas
│                                     │
│ 2. resize?                          │
│    calcResizeDims + multi-step scale │
│                                     │
│ 3. transform? (rotation + flip)     │
│    Canvas matrix transform           │
│                                     │
│ 4. watermark? (text or image)       │
│    Overlay render                    │
│                                     │
│ 5. encode()                         │
│    Canvas API: JPEG/WebP/PNG        │
│    WASM: AVIF (@jsquash), PNG (oxipng) │
│                                     │
│ 6. postMessage back to main thread  │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full pipeline runs in 50-200ms per image depending on size and format.&lt;/p&gt;




&lt;h2&gt;
  
  
  License Keys Instead of User Accounts
&lt;/h2&gt;

&lt;p&gt;I did NOT want to build auth. So the Pro model is dead simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User pays Creem ($24.99 lifetime)
    → Webhook fires
    → Server: generate "XXXX-XXXX-XXXX" code
    → Store in Redis: code → {email, devices[], created_at}
    → Resend: email with activation code
    → User enters code in app
    → Verified, device fingerprint registered
    → Max 5 devices per code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  SEO Landing Pages (11 Pages, $0 Hosting)
&lt;/h2&gt;

&lt;p&gt;11 SEO tool pages pre-rendered at build time. Each page targets a specific keyword:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/compress-png&lt;/td&gt;
&lt;td&gt;"compress PNG online"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/compress-jpeg&lt;/td&gt;
&lt;td&gt;"compress JPEG online"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/compress-webp&lt;/td&gt;
&lt;td&gt;"WebP compressor"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/compress-svg&lt;/td&gt;
&lt;td&gt;"SVG optimizer"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/remove-metadata&lt;/td&gt;
&lt;td&gt;"remove EXIF data"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/vs-tinypng&lt;/td&gt;
&lt;td&gt;"TinyPNG alternative"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;(6 more)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All static HTML. Vercel CDN handles them globally for free. Google is slowly indexing them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Numbers (After 1 Month)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&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;Total PV&lt;/td&gt;
&lt;td&gt;850&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total UV&lt;/td&gt;
&lt;td&gt;178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images compressed&lt;/td&gt;
&lt;td&gt;3,716&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro sales&lt;/td&gt;
&lt;td&gt;1 ($24.99)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not retiring on it. But it proves the model works: &lt;strong&gt;$0 burn rate means every sale is pure profit.&lt;/strong&gt;&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SEO pages earlier.&lt;/strong&gt; They're low effort, high leverage. Should have been day 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start Twitter sooner.&lt;/strong&gt; Build-in-public compounds. I started late.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't spend 3 hours debugging Worker &lt;code&gt;window&lt;/code&gt; access.&lt;/strong&gt; It's always the same bug.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Stack in One Line
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Next.js + Web Workers + Canvas API + Upstash Redis + Creem + Vercel = $0/month SaaS.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://compressfast.site" rel="noopener noreferrer"&gt;compressfast.site&lt;/a&gt; · &lt;a href="https://github.com/WML123270/compressfast" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #webdev #javascript #nextjs #saas #buildinpublic&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>buildinpublic</category>
    </item>
    <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>
  </channel>
</rss>
