图片压缩深度指南(2026):浏览器端无损/有损压缩原理与实战
为什么图片压缩是前端性能的基石
HTTP Archive 2026:图片占网页总字节的 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%。
有损压缩
- 色度子取样(4:2:0):色彩分辨率降一半,省 33%,肉眼无感。
- 量化:DCT 系数合并到同 bin。quality = 量化步长——越大越压缩。
- 预测编码:AVIF 67 种预测模式 vs WebP 4 种,压缩率更高。
格式选择指南
| 格式 | 有损/无损 | 透明 | 动画 | 覆盖率 | 压缩率(vs JPEG) |
|---|---|---|---|---|---|
| JPEG | 有损 | ❌ | ❌ | 99.9% | 基准 |
| PNG | 无损 | ✅ | ❌ | 99.9% | 照片最差 |
| WebP | 两者 | ✅ | ✅ | 97% | 25–35% 更小 |
| AVIF | 两者 | ✅ | ✅ | 93% | 50% 更小 |
决策:照片→WebP/AVIF 有损 0.75–0.85。图标→PNG/SVG。透明+照片→WebP/AVIF。动画→WebP/AVIF 比 GIF 小 80%+。
浏览器端压缩:Canvas + toBlob
async function compressImage(
file: File,
options: { maxWidth?: number; quality?: number; format?: string } = {}
): Promise<Blob> {
const { maxWidth = 1920, quality = 0.8, format = "image/webp" } = options;
const img = await createImageBitmap(file);
let { width, height } = img;
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
const canvas = document.createElement("canvas");
canvas.width = width; canvas.height = height;
const ctx = canvas.getContext("2d")!;
if (format === "image/jpeg") {
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, width, height);
}
ctx.drawImage(img, 0, 0, width, height);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error()), format, quality);
});
}
为什么先 resize?
4000×3000 在手机只需要 750px(2x retina)。砍宽度从 4000→750,直接省 95%+——任何算法都比不上。
质量参数建议
- WebP:0.7–0.85,低于 0.6 出现块效应。
- AVIF:0.4–0.6(范围 0–63,非线性的 0–100)。
- JPEG:0.75–0.9,超 0.92 回报递减严重。
Web Worker 多线程加速
// 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))
);
async function compressPool(files: File[]): Promise<Blob[]> {
return Promise.all(files.map((f, i) => new Promise((resolve) => {
const w = workers[i % workers.length];
w.onmessage = (e) => resolve(new Blob([e.data.buf], { type: e.data.type }));
w.postMessage({ file: f, options: { maxWidth: 1600, quality: 0.8 } });
})));
}
上传前预压缩
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) => {
if (file.size < 50 * 1024 && file.type === "image/png") return file;
const compressed = await new Promise<Blob>((resolve) => {
pool[idx % n].onmessage = (e) =>
resolve(new Blob([e.data.buf], { type: e.data.type }));
pool[idx % n].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`);
return new File([compressed], file.name.replace(/\.[^.]+$/, ".webp"), { type: "image/webp" });
})
);
await uploadToServer(results);
pool.forEach(w => w.terminate());
}
CI/CD 自动压缩(服务端兜底)
const sharp = require("sharp");
const files = await glob("public/images/**/*.{jpg,jpeg,png}");
await Promise.all(files.map(f =>
sharp(f).resize(1920, undefined, { withoutEnlargement: true })
.webp({ quality: 80, effort: 4 }).toFile(f.replace(/\.\w+$/, ".webp"))
));
# GitHub Actions
- name: Compress images
run: node scripts/compress-images.mjs
AVIF:未来已来,注意 3 个坑
- 编码极慢:effort=4 时 2–5 秒/图。CI 可,客户端不建议。
- 不支援渐进式:大图慢网先空白。用
<picture>fallback。 - 覆盖率 93%:
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="" loading="lazy" decoding="async">
</picture>
进阶技巧
分級品質策略
- 主圖:quality=0.85, maxWidth=1600
- 縮圖:quality=0.7, maxWidth=400
- 背景:quality=0.6, maxWidth=40 + CSS blur
漸進式載入(LQIP)
10×10 小圖拉伸佔位 → 全解析度版,大幅改善 CLS。
感知哈希去重
pHash + 汉明距离 < 10 → 复用旧 URL,节省计算。
FAQ
Q1:toBlob 为何返回 null?——Canvas 0×0 或跨域未设 CORS。img.crossOrigin = "anonymous"。
Q2:WebP/AVIF 的 quality 含义一样?——不同。WebP 80 ≈ AVIF 50(SSIM)。AVIF quality 设为 WebP 的 60–70%。
Q3:压缩 PNG 照片反而变大?——PNG 无损,高频纹理 DEFLATE 差。照片永远用有损。
Q4:toBlob vs toDataURL?——永远 toBlob。toDataURL Base64 膨胀 33% + 一次性内存。
Q5:客户端压缩会泄露隐私?——不会。全部在本地浏览器完成,无数据上传。
总结
策略:先缩尺寸 → 选格式 → 客户端预压 → 服务端兜底 → 分级品质。
#图片压缩#WebP#AVIF#Canvas#前端优化#无损压缩