The Complete Image Compression Guide (2026): Lossless & Lossy in the Browser

工具教程(Updated Jul 20, 2026)

Why image compression matters

HTTP Archive 2026: images account for 45% of page weight. A 4000×3000 photo can exceed 5 MB — WebP (quality=0.8) typically 200–400 KB, visually identical.

This goes deep into pixel-level principles, format tradeoffs, browser APIs, multi-threading, and CI/CD.


Lossy vs lossless

Lossless

  • PNG: DEFLATE + row filters. Great for flat colors, weak for photos.
  • WebP lossless: 26% smaller than PNG.
  • AVIF lossless: ~22% better than WebP lossless (AV1 intra-frame coding).

Lossy

  • Chroma subsampling (4:2:0): halve color resolution, 33% savings, imperceptible.
  • Quantization: DCT coefficients into bins. quality = quantization step.
  • Predictive coding: AVIF 67 prediction modes vs WebP 4 — hence higher compression.

Format selection

Format Lossy/Lossless Alpha Anim Coverage vs JPEG
JPEG Lossy 99.9% Baseline
PNG Lossless 99.9% Worst photo
WebP Both 97% 25–35% smaller
AVIF Both 93% 50% smaller

Rules: Photos→WebP/AVIF lossy 0.75–0.85. Icons→PNG/SVG. Transparency+photo→WebP/AVIF. Animation→WebP/AVIF (80%+ smaller than GIF).


Canvas + toBlob

async function compressImage(file: File, opts = {}): Promise<Blob> {
  const { maxWidth=1920, quality=0.8, format="image/webp" } = opts;
  const img = await createImageBitmap(file);
  let { width, height } = img;
  if (width > maxWidth) { height = Math.round(height*maxWidth/width); width = maxWidth; }
  const c = document.createElement("canvas");
  c.width=width; c.height=height;
  const ctx = c.getContext("2d")!;
  if (format==="image/jpeg") { ctx.fillStyle="#FFF"; ctx.fillRect(0,0,width,height); }
  ctx.drawImage(img,0,0,width,height);
  return new Promise((res,rej)=>c.toBlob(b=>b?res(b):rej(),format,quality));
}

Why resize first?

4000→750 (2x retina for 375px phone) = 95%+ direct savings — beats any algorithm.

Quality tips

  • WebP: 0.7–0.85 (blocking artifacts below 0.6).
  • AVIF: 0.4–0.6 (non-linear 0–63 scale).
  • JPEG: 0.75–0.9 (diminishing returns above 0.92).

Web Worker multi-threading

// compress.worker.ts
self.onmessage = async (e) => {
  const blob = await compressImage(e.data.file, e.data.options);
  const buf = await blob.arrayBuffer();
  self.postMessage({ buf, type: blob.type }, [buf]);
};
const workers = Array.from({length: navigator.hardwareConcurrency-1||1},
  () => new Worker(new URL("./compress.worker.ts", import.meta.url)));

Pre-upload compression

async function handleUpload(files: FileList) {
  const pool = Array.from({length: navigator.hardwareConcurrency-1||1},
    () => new Worker(new URL("./compress.worker.ts", import.meta.url)));
  
  const results = await Promise.all(Array.from(files).map(async (file, idx) => {
    if (file.size<50*1024 && file.type==="image/png") return file;
    const compressed = await new Promise<Blob>(r => {
      pool[idx%pool.length].onmessage = e => r(new Blob([e.data.buf], {type:e.data.type}));
      pool[idx%pool.length].postMessage({file, options:{maxWidth:1920,quality:0.78,format:"image/webp"}});
    });
    return new File([compressed], file.name.replace(/\.[^.]+$/,".webp"), {type:"image/webp"});
  }));
  
  await uploadToServer(results);
  pool.forEach(w=>w.terminate());
}

CI/CD (Sharp)

const sharp = require("sharp");
await sharp(f).resize(1920).webp({quality:80,effort:4}).toFile(out);
- run: node scripts/compress-images.mjs

AVIF: 3 footguns

  1. Very slow encode: 2–5s/image (0.3s for WebP).
  2. No progressive: blank until fully loaded.
  3. 93% coverage: <picture> fallback needed.
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="" loading="lazy">
</picture>

Advanced

Tiered quality: hero=0.85/1600, thumb=0.7/400, bg=0.6/40+CSS blur. LQIP: 10×10 placeholder → full. pHash dedup: Hamming<10 → reuse.


FAQ

Q1: toBlob null? — Canvas 0×0 or CORS missing. img.crossOrigin="anonymous".

Q2: WebP/AVIF quality same? — No. WebP 80 ≈ AVIF 50 (SSIM). AVIF quality = 60–70% of WebP.

Q3: PNG photo bigger? — Lossless fails on high-frequency. Always use lossy for photos.

Q4: toBlob vs toDataURL? — Always toBlob. toDataURL = Base64 +33% + all-at-once memory.

Q5: Privacy? — All local. No data leaves the browser during compression.


Conclusion

Strategy: resize → format → client pre-compress → server fallback → tiered quality.

#图片压缩#WebP#AVIF#Canvas#前端优化#无损压缩