图片压缩深度指南(2026):浏览器端无损/有损压缩原理与实战

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

为什么图片压缩是前端性能的基石

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 个坑

  1. 编码极慢:effort=4 时 2–5 秒/图。CI 可,客户端不建议。
  2. 不支援渐进式:大图慢网先空白。用 <picture> fallback。
  3. 覆盖率 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#前端优化#无损压缩