Optimizing images for the web usually forces a trade-off between two popular tools: TinyPNG’s cloud upload limits and Squoosh’s one-file-at-a-time workflow.
For more than a decade, TinyPNG has been the default quick fix for shrinking PNG and JPEG assets. Anyone managing production deployments quickly runs into its free-tier boundaries: 20 images per session, 5 MB per file, and the fact that every marketing asset, internal screenshot, or unreleased product photo is uploaded to a third-party server.
At the other end sits Google Chrome Labs’ Squoosh. Squoosh proved that mature codecs (MozJPEG, OxiPNG, WebP, AVIF) can run inside a browser via WebAssembly, but it was designed as an interactive lab for a single image. It has no batch queue, no shared settings across images, and no bulk archive export.
What sits between them is a client-side batch pipeline: multi-file throughput, zero install, and no image bytes leaving the device.
The TinyPNG Bottleneck: Session Quotas, Upload Latency, and Data Exposure
TinyPNG is a classic client-server SaaS. When you drop images into its web interface, your browser uploads each file over HTTPS to TinyPNG’s servers, where PNGs are palette-quantized (in the style of pngquant) and JPEGs are re-encoded, and you download the result. The output quality is excellent, but the architecture has trade-offs.
1. Free-Tier Quotas
At the time of writing, TinyPNG limits free web users to 20 images per session and 5 MB per file. Preparing a 120-image product catalog means six separate rounds. Removing those limits requires Web Pro ($39 per user per year, 75 MB file limit) or the developer API, which includes 500 free compressions per month and then bills per image (about $0.009 each for the next 9,500, with cheaper tiers above that).
2. Network Round-Trip Overhead
Every image makes a full round trip:
- Connection setup (DNS and TLS) with the remote endpoint, typically done once and reused across the batch.
- Uploading the original payload over your upstream connection, which is often much slower than downstream on residential and mobile links.
- Waiting for server-side processing.
- Downloading the re-encoded file.
For 20 camera photos at 4 MB each, that is 80 MB of upload before any compression starts. On a typical 10–20 Mbps uplink, that alone takes about 30–60 seconds.
3. Privacy and Compliance Exposure
Product screenshots containing staging URLs, customer records, or unannounced designs are a different risk class from stock photography. Depending on your organization’s policies, sending them to a third-party processor may require a data processing agreement (for example under GDPR Article 28) or vendor review. Raw camera files can also carry EXIF metadata, including GPS coordinates, which travels with the upload.
The Squoosh Paradox: Excellent Codecs, Single-File Workflow
Squoosh compiles mature C/C++ and Rust image libraries to .wasm modules and runs codecs like MozJPEG, WebP, AVIF, and OxiPNG inside the browser’s WebAssembly runtime. No images are uploaded.
flowchart TD
subgraph CloudModel["Traditional Cloud Compressor (TinyPNG)"]
A1["Client Browser"] -->|"1. Upload original image"| B1["Remote API Endpoint"]
B1 -->|"2. Server-side processing"| C1["Remote Worker (palette quantization / JPEG encode)"]
C1 -->|"3. Store result"| D1["Server Storage"]
D1 -->|"4. Download compressed file"| A1
end
subgraph LocalModel["In-Browser Execution (Utiliome)"]
A2["Local File in Browser RAM"] -->|"1. Native image decode"| B2["Canvas 2D Context"]
B2 -->|"2. Optional PNG posterization + toBlob encode"| C2["In-Memory Blob URL"]
C2 -->|"3. JSZip archive"| D2["Save to Local Disk"]
end
For real projects, though, the web app’s design creates friction:
- Single-image workflow: Optimizing 50 banner graphics means opening, tuning, and exporting each file one at a time.
- No ZIP export: With no batch queue, there is nothing to bundle into a single archive.
- No maintained batch tool: The official
@squoosh/cliandlibsquooshpackages, which once filled this gap, have been deprecated and removed from the GoogleChromeLabs repository. Only community forks remain.
Comparing the Architectures: TinyPNG vs. Squoosh vs. Utiliome
| Feature | TinyPNG (Web) | Squoosh (Web) | Utiliome |
|---|---|---|---|
| Execution Context | Remote servers | In-browser WebAssembly codecs | In-browser Canvas 2D (native browser encoders) |
| Image Upload | Every file uploaded | None | None |
| Batch Limit | 20 images per session (free) | Single file | No app-imposed cap (processed sequentially, bounded by device RAM) |
| Max File Size | 5 MB free / 75 MB Web Pro | Bounded by tab memory | Bounded by device memory and the browser’s max canvas size |
| Batch Export | Download all | Single-file download | One-click ZIP (compressed_images.zip) |
| iPhone HEIC Input | Yes (converted server-side) | Only where the browser decodes HEIC (e.g. Safari) | Yes (client-side heic2any, loaded on demand) |
| PNG Optimization | Palette quantization to 8-bit indexed PNG | OxiPNG lossless + optional palette reduction | Per-channel RGB posterization, then truecolor PNG |
| WebP Output | Yes (conversion feature) | Yes in all browsers (Wasm libwebp) | Chrome, Edge, Firefox (Safari’s canvas falls back to PNG) |
| Anti-Inflation Guard | Not documented | Manual inspection | Keeps original if same format/dimensions and not smaller |
| Pricing | Free tier; Web Pro $39/yr; metered API | Free (open source) | Free, no account |
Inside Utiliome’s In-Browser Compression Engine
Every claim below is taken from the compress-image widget source. The pipeline looks like this:
flowchart LR
Drop["Multi-File Dropzone<br/>(JPG, PNG, WebP, SVG, HEIC)"] --> Detect{"HEIC / HEIF MIME type or extension?"}
Detect -->|Yes| HeicConvert["Dynamic heic2any import<br/>(HEIC to JPEG at 0.92)"]
Detect -->|No| Decode["Native HTMLImageElement decode"]
HeicConvert --> Decode
Decode --> Canvas["Detached HTML5 Canvas 2D<br/>(optional max-dimension downscale)"]
Canvas --> FormatCheck{"Target format?"}
FormatCheck -->|"JPEG / WebP"| NativeBlob["canvas.toBlob()<br/>with quality factor"]
FormatCheck -->|"PNG, quality below 0.95"| Quantize["Per-channel RGB posterization<br/>(alpha untouched)"]
Quantize --> NativeBlob
NativeBlob --> AntiInflate{"Same format and dimensions,<br/>output not smaller?"}
AntiInflate -->|Yes| KeepOrig["Keep original file"]
AntiInflate -->|No| UseBlob["Use compressed blob"]
KeepOrig --> BatchList["Batch tabs and Before/After slider"]
UseBlob --> BatchList
BatchList --> Zip["JSZip generateAsync<br/>(compressed_images.zip)"]
1. Native Canvas 2D Encoding Instead of Downloaded Codecs
Rather than downloading WebAssembly codec binaries before the first compression, Utiliome uses the encoders already built into the browser’s rendering engine (Blink, WebKit, or Gecko), which rely on libraries such as libjpeg-turbo and libwebp. The widget draws each decoded image onto a canvas and calls:
canvas.toBlob(
(blob) => {
// Process optimized blob
},
targetMime,
settings.quality
);
The quality slider runs from 0.1 to 1.0 (10%–100%). Files are processed one after another on the main thread, not in parallel Web Workers, so very large batches take proportionally longer.
Safari caveat: Safari does not implement WebP encoding in
canvas.toBlob(). Per the HTML spec, it silently falls back to PNG, and the file is still named<name>_compressed.webpbecause the extension comes from the requested format. If you need WebP output, run the compressor in Chrome, Edge, or Firefox, or choose JPEG in Safari.
2. HEIC/HEIF Handling via Dynamic Import
iPhones save photos as HEIF/HEVC (.heic) by default, which most browsers cannot decode natively. Utiliome detects HEIC by MIME type or file extension (not by inspecting file signatures) and imports heic2any only when needed:
const convertHeicIfNeeded = async (file: File): Promise<File> => {
const isHeic =
file.type === 'image/heic' ||
file.type === 'image/heif' ||
/\.hei[cf]$/i.test(file.name);
if (!isHeic) return file;
const heic2any = (await import('heic2any')).default;
const converted = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.92 });
const blob = Array.isArray(converted) ? converted[0] : converted;
return new File([blob], file.name.replace(/\.hei[cf]$/i, '.jpg'), { type: 'image/jpeg' });
};
Because the import is dynamic, users compressing JPEG, PNG, or WebP never download the HEIC decoder. Note that HEIC files are first converted to a JPEG at 0.92 quality, and that intermediate JPEG becomes the compression input. It is also the baseline for the “Original” size, the savings badge, and the anti-inflation check, so the reported savings are measured against that JPEG, not the smaller .heic file.
3. PNG Compression: Per-Channel Posterization
canvas.toBlob('image/png', quality) ignores the quality argument, because PNG is lossless. A plain canvas re-export writes a truecolor PNG that is often larger than the source.
To make the quality slider meaningful for PNGs, Utiliome posterizes the raw ImageData when quality is below 0.95:
if (targetMime === 'image/png' && settings.quality < 0.95) {
try {
const imgData = ctx.getImageData(0, 0, outW, outH);
const data = imgData.data;
const step = Math.max(2, Math.round((1 - settings.quality) * 36));
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.round(data[i] / step) * step);
data[i + 1] = Math.min(255, Math.round(data[i + 1] / step) * step);
data[i + 2] = Math.min(255, Math.round(data[i + 2] / step) * step);
}
ctx.putImageData(imgData, 0, 0);
} catch {
// Ignore if canvas is tainted
}
}
Each red, green, and blue value snaps to the nearest multiple of step. The alpha byte (data[i + 3]) is never touched, so transparency is preserved exactly. Here is what the three presets produce, computed from the formula above:
| Preset | Quality | step | Levels per channel | Max distinct RGB colors |
|---|---|---|---|---|
| Max Savings | 0.45 | 20 | 14 | 2,744 |
| Balanced | 0.75 | 9 | 29 | 24,389 |
| High Quality | 0.90 | 4 | 65 | 274,625 |
Fewer distinct values means more repeated byte patterns after PNG’s row filtering, which DEFLATE compresses far better. Savings depend heavily on content: screenshots, UI mockups, and flat illustrations shrink the most, while photographic PNGs shrink less and can show banding in smooth gradients at Max Savings. Because the browser still writes a truecolor (not palette-indexed) PNG, a dedicated palette quantizer like TinyPNG or pngquant will often produce smaller files on flat graphics. For photographic content, converting to WebP or JPEG usually beats any PNG strategy.
4. Anti-Inflation Guard
Re-compressing an image that MozJPEG or Guetzli has already optimized can make it larger, because the second lossy pass has to encode the earlier compression artifacts as detail. Utiliome checks for this:
const sameFormat = targetMime === file.type;
const sameDims = outW === origW && outH === origH;
const finalBlob = sameFormat && sameDims && blob.size >= file.size ? file : blob;
The guard only applies when format and dimensions are unchanged. If you convert formats or downscale, the new blob is always used, since it is a different file by design.
5. In-Memory ZIP Packaging
For multi-file batches, Utiliome adds every successfully compressed blob to a JSZip instance and calls generateAsync({ type: 'blob' }) to build compressed_images.zip entirely on the client. A single-file session skips the ZIP and downloads the image directly as <name>_compressed.<ext>.
Production Workflow: Optimizing Web Assets at Scale
Compression is one stage of an image delivery pipeline. To improve Largest Contentful Paint (LCP), follow this order:
Step 1: Downscale Camera-Sized Images
Modern phones and DSLRs shoot at 12 to 48 megapixels (for example 6000×4000 px), producing files of several megabytes or more. Serving a 6000 px image in an 800 px blog column wastes bandwidth and decode time on mobile devices.
Scale assets to their maximum display size first (for example 1920 px for hero banners, 1280 px for content images). The compressor has built-in Max 1920px / 1280px / 800px presets, or use the Image Resizer for exact dimensions.
Step 2: Move Photographic Assets to WebP
Google’s published comparisons report WebP lossy files about 25–34% smaller than JPEG at equivalent SSIM, and lossless WebP about 26% smaller than PNG. Use the Format Converter for bulk format changes, keeping Safari’s encoding limitation in mind.
Step 3: Run Batch Compression
Drop the scaled images into the Image Compressor and pick one of three presets:
- Max Savings (45%): Thumbnail grids, background textures, and mobile previews where bytes matter most.
- Balanced (75%, default): Blog photography, editorial graphics, and social cards.
- High Quality (90%): Portfolios and e-commerce zoom images where gradients and fine detail must survive.
Inspect edges, text, and shadows with the Before/After split slider. Then click Apply settings to all N images (shown once two or more files are loaded) to copy the active tab’s settings to the whole batch, and download everything as a ZIP.
5-Point Pre-Flight Checklist for Web Images
Before pushing compressed images to your CDN or web server:
- Confirm no uploads: Open DevTools (
F12, orCmd+Option+Ion macOS), go to the Network tab, and compress a batch. No request should carry image data to an external host. - Match dimensions to containers: No asset should exceed about 2× its display width (to cover high-DPI screens). A 960 px container needs at most a 1920 px image.
- Check transparency after JPEG conversion: JPEG has no alpha channel. Utiliome fills transparent areas with
#FFFFFFwhen the target is JPEG, so logos meant for dark backgrounds should stay PNG or WebP. - Set a page weight budget: Core Web Vitals measure timing (LCP should be 2.5 s or less), not bytes. A practical internal budget, such as keeping above-the-fold images under a few hundred KB, helps you hit that target on mobile networks.
- Review “Already optimal size” results: The savings badge never shows a negative number. “Already optimal size” appears whenever the rounded savings are 0%. When format and dimensions are unchanged, that usually means the anti-inflation guard kept the original (or the gain was under 0.5%). If you switched output format, the new file is used even when it is larger, so compare the Original and Compressed sizes before downloading. Try a lower quality, a smaller max dimension, or WebP output.
Replacing session-capped cloud uploads and one-file-at-a-time tuning with local, in-browser batch processing keeps sensitive assets on your device and takes the quota math out of shipping a media library.