圖片壓縮深度指南(2026):瀏覽器端無損/有損壓縮原理與實戰

工具教程(更新於 2026年7月20日)

為什麼圖片壓縮是前端效能的基石

根據 2026 年 HTTP Archive 資料,圖片佔網頁平均總位元組的 45%。一張未壓縮的 4000×3000 照片可以輕易超過 5 MB——而同樣的圖在 WebP (quality=0.8) 下通常只需 200–400 KB,肉眼無法分辨差異。

本文深入像素級原理、格式取捨、瀏覽器 API 實作、多執行緒加速,以及如何把圖片壓縮嵌入 CI/CD 流水線。


有損 vs 無損:底層原理

無損壓縮

無損壓縮保證逐位元還原——壓縮後的資料解壓回來與原始完全一致。核心技術:

  • PNG:DEFLATE 演算法 + 行過濾器。對顏色均勻區域效果極佳,但對照片般的高頻紋理效果有限。
  • WebP 無損:使用更先進的預測編碼和色彩快取,通常比 PNG 小 26%。
  • AVIF 無損:基於 AV1 幀內編碼,無損壓縮率比 WebP 無損高約 22%。

有損壓縮

有損壓縮捨棄人眼不易察覺的資訊來換取體積:

  • 色度子取樣(Chroma Subsampling):人眼對亮度比對色彩敏感。把色彩解析度降為亮度的一半(4:2:0),肉眼幾乎無感,體積省 33%。
  • 量化(Quantization):把相近的 DCT 係數合併到同一個 bin。品質參數本質上就是量化步長——步長越大,更多係數被壓縮為零,體積越小,細節越少。
  • 預測編碼:WebP/AVIF 使用幀內預測,利用相鄰像素塊的相關性。AVIF 的預測模式(67 種)遠多於 WebP(4 種),這是它壓縮率更高的原因。

格式選擇指南

格式 有損/無損 透明通道 動畫 瀏覽器覆蓋率 壓縮率(vs JPEG)
JPEG 有損 99.9% 基準
PNG 無損 99.9% 照片最差
WebP 兩者 97% 25–35% 更小
AVIF 兩者 93% 50% 更小

決策規則

  • 照片 → WebP/AVIF 有損,quality 0.75–0.85。
  • 圖示/線條/UI → PNG 無損 或 SVG(向量優先)。
  • 需要透明 + 照片 → WebP/AVIF(支援透明有損)
  • 需要動畫 → WebP/AVIF 動態(比 GIF 小 80%+)

瀏覽器端壓縮:Canvas + toBlob

HTMLCanvasElement.toBlob() 是瀏覽器端圖片壓縮的核心 API:

async function compressImage(
  file: File,
  options: {
    maxWidth?: number;
    quality?: number;
    format?: "image/webp" | "image/jpeg" | "image/avif";
  } = {}
): Promise<Blob> {
  const { maxWidth = 1920, quality = 0.8, format = "image/webp" } = options;

  // 1. 解碼原圖
  const img = await createImageBitmap(file);

  // 2. 計算縮放尺寸(保持長寬比)
  let { width, height } = img;
  if (width > maxWidth) {
    height = Math.round((height * maxWidth) / width);
    width = maxWidth;
  }

  // 3. 繪製到 Canvas
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d")!;

  // 如果要輸出為 JPEG(不支援透明),先填充白色背景
  if (format === "image/jpeg") {
    ctx.fillStyle = "#FFFFFF";
    ctx.fillRect(0, 0, width, height);
  }

  ctx.drawImage(img, 0, 0, width, height);

  // 4. 匯出
  return new Promise((resolve, reject) => {
    canvas.toBlob(
      (blob) => (blob ? resolve(blob) : reject(new Error("toBlob failed"))),
      format,
      quality
    );
  });
}

為什麼要 resize?

一張 4000×3000 的照片在 375px 寬的手機螢幕上顯示,實際只需要 ~750px 寬(2x retina)。把寬度從 4000 砍到 750,體積直接減少 95% 以上——這比任何壓縮演算法都有效。

品質參數建議

  • WebP:0.7–0.85 為最佳區間。低於 0.6 開始出現明顯塊效應(blocking artifacts)。
  • AVIF:0.4–0.6 對應 WebP 的 0.7–0.85。AVIF 的 quality 範圍為 0–63,與 WebP 的 0–100 非線性對應。
  • JPEG:0.75–0.9,不建議超過 0.92(體積回報遞減嚴重)。

Web Worker 多執行緒加速

當你壓縮 20 張照片時,在主執行緒做會讓你頁面卡死 5–10 秒。把壓縮移到 Worker 中:

// compress.worker.ts
self.onmessage = async (e: MessageEvent<{ file: File; options: CompressOptions }>) => {
  const { file, options } = e.data;
  const blob = await compressImage(file, options);
  const arrayBuffer = await blob.arrayBuffer();
  self.postMessage({ arrayBuffer, type: blob.type }, [arrayBuffer]);
};

主執行緒:

const workers = Array.from(
  { length: navigator.hardwareConcurrency - 1 || 1 },
  () => new Worker(new URL("./compress.worker.ts", import.meta.url))
);

async function compressInWorker(file: File, index: number): Promise<Blob> {
  return new Promise((resolve) => {
    const w = workers[index % workers.length];
    w.onmessage = (e) => resolve(new Blob([e.data.arrayBuffer], { type: e.data.type }));
    w.postMessage({ file, options: { maxWidth: 1600, quality: 0.8 } });
  });
}

// 並行處理
const files = Array.from(input.files);
const compressed = await Promise.all(files.map((f, i) => compressInWorker(f, i)));

使用 navigator.hardwareConcurrency 來決定 Worker 池大小(通常 threads - 1 留一個給主執行緒)。


上傳前預壓縮:完整整合範例

async function handleUpload(files: FileList) {
  const n = navigator.hardwareConcurrency - 1 || 1;
  const pool = Array.from({ length: n }, () =>
    new Worker(new URL("./compress.worker.ts", import.meta.url))
  );

  const results = await Promise.all(
    Array.from(files).map(async (file, idx) => {
      // GIF/PNG icon 跳過壓縮(小於 50KB)
      if (file.size < 50 * 1024 && file.type === "image/png") {
        return file;
      }

      const compressed = await new Promise<Blob>((resolve) => {
        const w = pool[idx % n];
        w.onmessage = (e) => resolve(new Blob([e.data.arrayBuffer], { type: e.data.type }));
        w.postMessage({ file, options: { maxWidth: 1920, quality: 0.78, format: "image/webp" } });
      });

      console.log(
        `${file.name}: ${(file.size / 1024).toFixed(0)}KB → ${(compressed.size / 1024).toFixed(0)}KB ` +
        `(${((1 - compressed.size / file.size) * 100).toFixed(0)}% saved)`
      );

      return new File(
        [compressed],
        file.name.replace(/\.[^.]+$/, ".webp"),
        { type: "image/webp" }
      );
    })
  );

  await uploadToServer(results);
  pool.forEach((w) => w.terminate());
}

CI/CD 自動壓縮(伺服器端兜底)

客戶端壓縮依賴使用者裝置效能和瀏覽器支援。伺服器端兜底是必要的。用 Sharp(Node.js 最快的圖片處理庫):

const sharp = require("sharp");

async function serverCompress(inputPath, outputPath) {
  await sharp(inputPath)
    .resize(1920, undefined, { withoutEnlargement: true })
    .webp({ quality: 80, effort: 4 })  // effort 1–6, 越高压縮率越高但越慢
    .toFile(outputPath);
}

// 批次處理
const files = await glob("public/images/**/*.{jpg,jpeg,png}");
await Promise.all(
  files.map((f) => serverCompress(f, f.replace(/\.(jpe?g|png)$/i, ".webp")))
);

在 GitHub Actions 中嵌入壓縮步驟:

- name: Compress images
  run: node scripts/compress-images.mjs
- name: Commit compressed images
  run: |
    git config user.name "bot"
    git add public/images/**/*.webp
    git commit -m "chore: compress images" || true

AVIF:未來已來,但要注意坑

AVIF 的壓縮率是 WebP 的兩倍,但有三個坑:

  1. 編碼極慢sharp 的 AVIF 編碼以 effort: 4 為例,一張圖要 2–5 秒(WebP 在 0.3 秒內)。CI 做沒問題,客戶端即時壓縮不建議。effort: 9(最高)一張圖可達 30 秒。
  2. 漸進式渲染:AVIF 不支援漸進式解碼(progressive decoding),大圖在慢網路下會先顯示空白,直到檔案完全下載完畢。對 UX 不友好。
  3. 瀏覽器覆蓋:2026 年約 93%,對於必須支援舊版 iOS Safari 或 IE 的場景,用 <picture> 標籤提供 WebP fallback:
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="fallback" loading="lazy" decoding="async">
</picture>

進階技巧

感知雜湊去重

在壓縮前用感知雜湊(pHash)偵測圖片是否重複(如使用者重複上傳同一張照片)。若與已存在圖片的漢明距離 < 10,跳過壓縮並重複使用舊 URL——節省計算資源。

分級品質策略

不同位置的圖片用不同品質:

  • 商品主圖:quality=0.85, maxWidth=1600。
  • 縮圖網格:quality=0.7, maxWidth=400。
  • 背景底圖:quality=0.6, maxWidth=40(極端縮小 + CSS blur 濾鏡)。

漸進式載入(LQIP)

先用 10×10 的小圖拉伸顯示做佔位(LQIP——Low Quality Image Placeholder),再載入全解析度版本。這大幅改善 CLS(Cumulative Layout Shift)。


常見問題(FAQ)

Q1:為什麼 toBlob 有時回傳 null?

當 Canvas 的尺寸為 0×0 或包含跨域圖片(CORS 未設定)時。確保 Canvas 有正尺寸,且對跨域圖片設定了 img.crossOrigin = "anonymous"

Q2:WebP 和 AVIF 的 quality 參數含義一樣嗎?

不一樣。WebP quality=80 對應的 SSIM 與 AVIF quality=50 大致相當。AVIF 的 quality 曲線更陡(0–63),建議將 AVIF quality 設為 WebP quality 的 60–70%。

Q3:為什麼壓縮 PNG 照片體積反而變大?

PNG 是無損格式,照片的高頻紋理(大片樹葉、水面波紋)DEFLATE 壓縮不好。照片永遠用有損格式(WebP/AVIF/JPEG)。

Q4:toBlob 和 toDataURL 選誰?

永遠選 toBlobtoDataURL 返回 Base64 字串,體積膨脹 33%,且必須一次性分配整塊記憶體。toBlob 返回二進位 Blob,直接上傳或下載。

Q5:客戶端壓縮會洩漏使用者隱私嗎?

不會。所有處理都在使用者本機瀏覽器中完成——Canvas 渲染、toBlob 壓縮——沒有任何圖片資料離開使用者裝置。這也是瀏覽器端壓縮相比上傳到伺服器再壓縮的核心安全優勢。


總結

圖片壓縮不是「調低 quality 就行」。它是一個組合策略:

  1. 先縮尺寸——砍掉不需要的解析度,效果遠超任何壓縮演算法。
  2. 再選格式——照片用 WebP/AVIF 有損;圖標用 PNG/SVG 無損。
  3. 客戶端預壓縮——上傳前在瀏覽器裡用 Canvas + Web Worker 處理,省頻寬、加速。
  4. 服務端兜底——Sharp 在 CI/CD 裡保證統一標準。
  5. 分級策略——主圖、縮圖、背景圖使用不同品質和尺寸。
#图片压缩#WebP#AVIF#Canvas#前端优化#无损压缩