<?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: Luca Sammarco</title>
    <description>The latest articles on DEV Community by Luca Sammarco (@samma1997).</description>
    <link>https://dev.to/samma1997</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F909685%2F7b98ae37-809a-40ce-830c-a9f52dbd5ebe.jpeg</url>
      <title>DEV Community: Luca Sammarco</title>
      <link>https://dev.to/samma1997</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samma1997"/>
    <language>en</language>
    <item>
      <title>I Used AI to Rename 71 Travel Photos for SEO — Here's What Happened</title>
      <dc:creator>Luca Sammarco</dc:creator>
      <pubDate>Wed, 25 Mar 2026 23:20:09 +0000</pubDate>
      <link>https://dev.to/samma1997/i-used-ai-to-rename-71-travel-photos-for-seo-heres-what-happened-2c18</link>
      <guid>https://dev.to/samma1997/i-used-ai-to-rename-71-travel-photos-for-seo-heres-what-happened-2c18</guid>
      <description>&lt;p&gt;I came back from Sri Lanka with 71 photos — all named IMG_3570.JPG&lt;/p&gt;

&lt;p&gt;Sixteen days of travel, 71 iPhone shots. Every single file with a generic name, from IMG_3570.JPG to IMG_5018.JPG.&lt;/p&gt;

&lt;p&gt;If you've ever uploaded photos to a blog, portfolio, or e-commerce site, you know the problem: generic filenames are invisible to search engines.&lt;/p&gt;

&lt;p&gt;Google Images can't understand what IMG_3570.JPG contains. But gangaramaya-temple-buddha-statues-colombo-sri-lanka.jpg? That ranks.&lt;/p&gt;

&lt;p&gt;The problem: 71 files, zero SEO value&lt;/p&gt;

&lt;p&gt;Here's what my camera roll looked like: IMG_3570.JPG, IMG_3572.JPG, IMG_3574.JPG, IMG_3583.JPG... 71 files total.&lt;/p&gt;

&lt;p&gt;Manually renaming 71 photos with descriptive, keyword-rich filenames? That's easily 2+ hours of tedious work. I know because I've done it before.&lt;/p&gt;

&lt;p&gt;The solution: AI reads the photo and names it&lt;br&gt;
I built SammaPix AI Rename specifically for this. It uses Google Gemini to analyze what's actually in the image and generates an SEO-optimized filename.&lt;/p&gt;

&lt;p&gt;Here are real before/after examples from my Sri Lanka trip:&lt;/p&gt;

&lt;p&gt;🛕 Temples &amp;amp; Sacred Sites&lt;br&gt;
IMG_3570.JPG → gangaramaya-temple-buddha-statues-stupa-colombo-sri-lanka.jpg&lt;br&gt;
IMG_3583.JPG → gangaramaya-temple-interior-golden-buddha-colombo.jpg&lt;br&gt;
IMG_3754.JPG → kelaniya-raja-maha-vihara-stone-carvings-gampaha.jpg&lt;br&gt;
IMG_3843.JPG → dambulla-cave-temple-reclining-buddha-ancient-murals.jpg&lt;/p&gt;

&lt;p&gt;🌿 Landscapes &amp;amp; Nature&lt;br&gt;
IMG_3867.JPG → pidurangala-rock-panoramic-jungle-view-sigiriya.jpg&lt;br&gt;
IMG_4155.JPG → nuwara-eliya-tea-plantation-misty-hills-sri-lanka.jpg&lt;br&gt;
IMG_4458.JPG → nine-arches-bridge-ella-train-crossing-sri-lanka.jpg&lt;br&gt;
IMG_4785.JPG → mirissa-coconut-tree-hill-ocean-sunset.jpg&lt;/p&gt;

&lt;p&gt;👥 People &amp;amp; Culture&lt;br&gt;
IMG_3622.JPG → maharagama-elephant-keeper-gentle-giant-sri-lanka.jpg&lt;br&gt;
IMG_3765.JPG → colombo-pettah-market-street-electric-wires.jpg&lt;br&gt;
IMG_4998.JPG → negombo-fisherman-traditional-boat-morning-catch.jpg&lt;/p&gt;

&lt;p&gt;Every filename is descriptive (tells Google exactly what's in the image), keyword-rich (includes location, subject, and context), hyphenated (proper URL format for SEO), and unique (no two files have the same name).&lt;/p&gt;

&lt;p&gt;Why filenames matter for SEO&lt;/p&gt;

&lt;p&gt;Google's own documentation says image filenames are a ranking signal for Google Images. The filename is one of the first things Googlebot reads when crawling an image.&lt;/p&gt;

&lt;p&gt;A study by Backlinko found that descriptive image filenames correlate with higher Google Images rankings. It's one of the easiest SEO wins — yet most people ignore it because renaming files manually is painful.&lt;/p&gt;

&lt;p&gt;The technical approach&lt;br&gt;
The AI Rename tool works like this:&lt;/p&gt;

&lt;p&gt;Drop images into the browser (nothing gets uploaded to a server)&lt;/p&gt;

&lt;p&gt;A compressed thumbnail is sent to Google Gemini Flash&lt;br&gt;
Gemini analyzes the image content, identifies subjects, location cues, and context&lt;/p&gt;

&lt;p&gt;It generates a slug-formatted filename optimized for SEO&lt;br&gt;
Download the renamed files&lt;/p&gt;

&lt;p&gt;💡 The key insight: the AI doesn't just describe the image — it generates a filename specifically structured for search engines. It uses hyphens, includes relevant keywords, and follows Google's recommended format.&lt;/p&gt;

&lt;p&gt;Results: 71 photos renamed in under 3 minutes&lt;br&gt;
The entire batch was processed in about 2.5 minutes. Each photo got a unique, descriptive filename that actually means something to search engines. For comparison: manual renaming would have taken ~2 hours. That's a 97% time saving.&lt;/p&gt;

&lt;p&gt;The full workflow I use&lt;br&gt;
For my travel photography, I chain three tools together:&lt;/p&gt;

&lt;p&gt;🏷️ AI Rename → descriptive SEO filenames&lt;br&gt;
📦 Compress → reduce file size by 60–70% without visible quality loss&lt;br&gt;
🖼️ WebP Convert → modern format, additional 25–30% size reduction&lt;/p&gt;

&lt;p&gt;Total processing time for 71 photos: about 5 minutes. All in the browser, no uploads to any server.&lt;/p&gt;

&lt;p&gt;SammaPix AI Rename is free for 5 renames/day. The photos from this article are in my Sri Lanka portfolio.&lt;/p&gt;

&lt;p&gt;What's your current workflow for naming image files? I'm curious if anyone else has automated this.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>seo</category>
      <category>photography</category>
    </item>
    <item>
      <title>How I Built 20 Browser-Based Image Tools Without a Single Server Upload</title>
      <dc:creator>Luca Sammarco</dc:creator>
      <pubDate>Sat, 21 Mar 2026 08:15:15 +0000</pubDate>
      <link>https://dev.to/samma1997/how-i-built-20-browser-based-image-tools-without-a-single-server-upload-242k</link>
      <guid>https://dev.to/samma1997/how-i-built-20-browser-based-image-tools-without-a-single-server-upload-242k</guid>
      <description>&lt;p&gt;When I started building SammaPix, I had a decision to make: process images on a server like everyone else, or try something unconventional—handle everything in the browser.&lt;/p&gt;

&lt;p&gt;The server approach seemed logical. Better performance, easier scaling, industry standard. But then I thought: why would I upload someone's family photos, medical documents, or confidential screenshots to my servers when I could process them right there in their browser?&lt;/p&gt;

&lt;p&gt;That question led me down a rabbit hole of Canvas APIs, Web Workers, WebAssembly, and hard lessons about browser limitations. Here's what I learned building 20+ image tools entirely client-side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bxprb71qqwa92g0sw5e.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bxprb71qqwa92g0sw5e.png" alt="SammaPix Compress Tool — browser-based image compression" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The compress tool: drop images, adjust quality, download — all in your browser.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Client-Side Processing Isn't Actually Crazy
&lt;/h2&gt;

&lt;p&gt;The moment I decided to go client-side, everyone asked the same question: "But doesn't the server make it faster?"&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Here's the math: uploading a 5MB image to a server (200ms), processing it (300ms), downloading the result (200ms) = 700ms. Meanwhile, processing that same image in the browser with modern JavaScript? 150-400ms depending on the operation.&lt;/p&gt;

&lt;p&gt;The bandwidth elimination is huge. But the real win isn't speed—it's privacy.&lt;/p&gt;

&lt;p&gt;I could promise users their images stay private. Not "we delete them after 24 hours" or "they're encrypted in transit." Actually private. The image never leaves their device. Full stop.&lt;/p&gt;

&lt;p&gt;That's not a marketing angle—that's the entire architecture.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture: What Actually Runs in the Browser
&lt;/h2&gt;

&lt;p&gt;SammaPix has 20 tools. Most of them live entirely in client-side JavaScript:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0f5qwu835zsnwbjk9p0.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0f5qwu835zsnwbjk9p0.png" alt="SammaPix — 20+ browser-based image tools" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All 20 tools, organized by category.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image compression&lt;/strong&gt; - Using &lt;code&gt;browser-image-compression&lt;/code&gt; library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format conversion&lt;/strong&gt; - Canvas API for PNG, JPEG, WebP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resize and crop&lt;/strong&gt; - Canvas transformation matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate, flip, invert&lt;/strong&gt; - Canvas filters and pixel manipulation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blur, sharpen, brightness&lt;/strong&gt; - Canvas filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt; - Web Workers to avoid blocking the UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is 80% of the functionality. Here's a simple example of how compression works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;imageCompression&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser-image-compression&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;compressImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxSizeMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxWidthOrHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;useWebWorker&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="k"&gt;try&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;compressedFile&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;imageCompression&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;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;compressedFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Compression failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&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;That library handles the heavy lifting: quality reduction, JPEG chroma subsampling, WebP encoding where supported. It's battle-tested and open source.&lt;/p&gt;

&lt;p&gt;For basic Canvas operations, the pattern is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rotateImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;degrees&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&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;rad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;degrees&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="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&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;imageElement&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="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;imageElement&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&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/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&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;Canvas gives you a 2D drawing context. You transform it, draw the image, and export as a data URL. It's that simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HEIC Problem (And Why I Added WebAssembly)
&lt;/h2&gt;

&lt;p&gt;Then came iOS users.&lt;/p&gt;

&lt;p&gt;When someone uploads an image from an iPhone, they get HEIC format. Browser support for HEIC is... let's say "limited." Safari handles it. Chrome? Not reliably. Firefox? No.&lt;/p&gt;

&lt;p&gt;I had three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tell iPhone users "sorry, use PNG"&lt;/li&gt;
&lt;li&gt;Upload to server to convert HEIC&lt;/li&gt;
&lt;li&gt;Use WebAssembly to decode HEIC in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I chose option 3.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;heic2any&lt;/code&gt; library wraps WebAssembly to decode HEIC without hitting a server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;heic2any&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleHEICUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nx"&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/heic&lt;/span&gt;&lt;span class="dl"&gt;'&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;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/heif&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;try&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;convertedBlob&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="na"&gt;blob&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="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/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;convertedBlob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEIC conversion failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&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="nx"&gt;file&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;The WebAssembly module (~300KB) gets fetched once and cached. Subsequent HEIC uploads use the cached version. The trade-off: first load includes that overhead, but after that, users own the entire conversion pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Server Still Matters: AI Features
&lt;/h2&gt;

&lt;p&gt;Here's where I'm honest: some features &lt;em&gt;need&lt;/em&gt; a backend.&lt;/p&gt;

&lt;p&gt;When a user asks SammaPix to automatically rename an image based on its content, or generate alt-text, those features call Google Gemini Flash API. The image gets sent to Google, briefly analyzed, and the text comes back.&lt;/p&gt;

&lt;p&gt;I don't lie about this. The app clearly indicates which tools use AI and what that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This tool sends the image to Google Gemini API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiTools&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto-rename&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serverRequired&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;privacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analyzed by Google&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alt-text-generator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serverRequired&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;privacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analyzed by Google&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;// Everything else runs client-side&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientTools&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serverRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convert-format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serverRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;batch-resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serverRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 18 others&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For AI features, I send only what's necessary (the image), not any metadata, and I don't log or store the results. Google's API handles privacy per their terms. Users can choose to skip those features entirely and stay 100% local.&lt;/p&gt;

&lt;p&gt;This hybrid approach lets me offer intelligence without violating the privacy promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: Processing 500 Images in the Browser
&lt;/h2&gt;

&lt;p&gt;The next challenge was batch processing. If someone uploads 500 images, processing them sequentially locks the UI. Each image takes 200-500ms, so 500 images = 2-4 minutes of frozen interface. Unacceptable.&lt;/p&gt;

&lt;p&gt;Web Workers solve this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/imageWorker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addToBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;imageQueue&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;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processBatch&lt;/span&gt;&lt;span class="p"&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="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compress-batch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxSizeMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWidthOrHeight&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="p"&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;event&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&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="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;downloadProcessedImage&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now processing happens on a separate thread. UI stays responsive. Users see progress in real-time.&lt;/p&gt;

&lt;p&gt;In practice, this handles 500 images without breaking a sweat. I've tested it with 1000+ images and the browser stays responsive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory Management: The Real Gotcha
&lt;/h2&gt;

&lt;p&gt;Here's what nobody tells you about client-side image processing: memory.&lt;/p&gt;

&lt;p&gt;When you decompress a JPEG into Canvas, it expands to raw pixels. A 2MB JPEG becomes 8-12MB in memory (because each pixel is 4 bytes: RGBA).&lt;/p&gt;

&lt;p&gt;Process 50 high-res images in parallel and you're looking at 400-600MB in memory. Mobile browsers start garbage-collecting aggressively. Desktop browsers slow down.&lt;/p&gt;

&lt;p&gt;The solution: process sequentially with cleanup between each file, and set explicit canvas memory limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_CANVAS_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Prevent 8K images from exploding memory&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;constrainImageDimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&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="nx"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_CANVAS_SIZE&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;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxSize&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;ratio&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="nx"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;height&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;width&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;floor&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;ratio&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;height&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&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="nx"&gt;width&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the browser doesn't grind to a halt on 8K images or extreme batch sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;After several months of SammaPix in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1M+ images compressed&lt;/strong&gt; via browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero server costs&lt;/strong&gt; for image processing (only API calls for AI features)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.2% success rate&lt;/strong&gt; (failures mostly user-environment issues: out of memory, browser crashes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average processing time&lt;/strong&gt;: 250ms per image compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile support&lt;/strong&gt;: Works on iOS Safari, Chrome, Firefox&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Client-side processing isn't inherently better. It's a trade-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Genuine privacy (images never leave device)&lt;/li&gt;
&lt;li&gt;No server infrastructure costs&lt;/li&gt;
&lt;li&gt;Faster for simple operations&lt;/li&gt;
&lt;li&gt;Offline capable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited by device hardware&lt;/li&gt;
&lt;li&gt;Harder to implement complex features&lt;/li&gt;
&lt;li&gt;Browser inconsistencies (especially mobile)&lt;/li&gt;
&lt;li&gt;Users bear the computational cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if privacy is important to your users, if you want to eliminate data storage liability, or if you want to build a tool that works offline—client-side is worth the complexity.&lt;/p&gt;

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

&lt;p&gt;If I built this again:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use a service worker&lt;/strong&gt; earlier (not just for offline, but for caching and resource management)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement SharedArrayBuffer&lt;/strong&gt; for true multi-threaded processing (when browsers support it widely)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile memory usage&lt;/strong&gt; from day one (memory leaks hide in browser tools until you hit them at scale)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be transparent about AI features&lt;/strong&gt; from the start (I added this later; it should be baked into the UX)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on real devices&lt;/strong&gt; not just emulators (mobile behavior is different)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Privacy Argument
&lt;/h2&gt;

&lt;p&gt;Here's the thing: most image tools send your images to a server. They'll tell you it's encrypted, deleted after 24 hours, GDPR compliant.&lt;/p&gt;

&lt;p&gt;And maybe that's true. But wouldn't you rather not have to trust that?&lt;/p&gt;

&lt;p&gt;SammaPix doesn't need your trust. The image never leaves. You can disable your network, run the app offline, and it still works. No backdoors. No data collection. No ToS change surprises.&lt;/p&gt;

&lt;p&gt;That's not just a feature—it's a different category of application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Building tools entirely in the browser forces you to learn the platform deeply. Canvas APIs, Web Workers, WebAssembly, service workers—you touch all of them.&lt;/p&gt;

&lt;p&gt;Is it harder than a server-side equivalent? Sometimes. Is it worth it? Ask the developers who've built client-side tools while keeping infrastructure costs near zero.&lt;/p&gt;

&lt;p&gt;The browser is powerful. We forget that sometimes.&lt;/p&gt;




&lt;p&gt;If you want to see all this in action, check out &lt;a href="https://www.sammapix.com" rel="noopener noreferrer"&gt;SammaPix&lt;/a&gt; — it's free and open to try.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with Next.js 14, Canvas API, and way too much time in the browser DevTools.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>canvas</category>
    </item>
  </channel>
</rss>
