圖片壓縮深度指南(2026):瀏覽器端無損/有損壓縮原理與實戰
為什麼圖片壓縮是前端效能的基石
根據 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 的兩倍,但有三個坑:
- 編碼極慢:
sharp的 AVIF 編碼以effort: 4為例,一張圖要 2–5 秒(WebP 在 0.3 秒內)。CI 做沒問題,客戶端即時壓縮不建議。effort: 9(最高)一張圖可達 30 秒。 - 漸進式渲染:AVIF 不支援漸進式解碼(progressive decoding),大圖在慢網路下會先顯示空白,直到檔案完全下載完畢。對 UX 不友好。
- 瀏覽器覆蓋: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 選誰?
永遠選 toBlob。toDataURL 返回 Base64 字串,體積膨脹 33%,且必須一次性分配整塊記憶體。toBlob 返回二進位 Blob,直接上傳或下載。
Q5:客戶端壓縮會洩漏使用者隱私嗎?
不會。所有處理都在使用者本機瀏覽器中完成——Canvas 渲染、toBlob 壓縮——沒有任何圖片資料離開使用者裝置。這也是瀏覽器端壓縮相比上傳到伺服器再壓縮的核心安全優勢。
總結
圖片壓縮不是「調低 quality 就行」。它是一個組合策略:
- 先縮尺寸——砍掉不需要的解析度,效果遠超任何壓縮演算法。
- 再選格式——照片用 WebP/AVIF 有損;圖標用 PNG/SVG 無損。
- 客戶端預壓縮——上傳前在瀏覽器裡用 Canvas + Web Worker 處理,省頻寬、加速。
- 服務端兜底——Sharp 在 CI/CD 裡保證統一標準。
- 分級策略——主圖、縮圖、背景圖使用不同品質和尺寸。