2.4 图片处理:批量压缩与 WebP 转换
本节是第二篇的收尾,也是整个构建链路里投入产出比最高的一环。
创业页第二个月,设计交付了首屏视觉:一张 4.2MB 的 PNG,移动端打开要 3.8 秒。运营同学问能不能“把图片压一下但别失真”,我打开在线压缩网站一张张传,传到第 6 张时意识到这事必须自动化。本节记录我们用 Node 加 sharp 搭的图片流水线:批量压缩、转 WebP、HTML 回退,顺带把 CDN 流量费也降了下来。
2.4.1 先量再优:图片占包体多少
打开 Chrome DevTools 的 Network 面板统计首屏请求:5.4MB 里 4.8MB 是图片,JS 压缩后只有 180KB。结论很明确——优化图片的 ROI 最高,远高于继续抠 JS 体积。又翻了 云监控 一周的流量数据:图片占 CDN 回源流量的八成。这组数字后来在第五篇的成本复盘里直接成为预算依据。
2.4.2 格式选择:为什么是 WebP
| 格式 | 相对体积 | 透明通道 | 浏览器支持 | 用途 |
|---|---|---|---|---|
| PNG | 100% | 支持 | 全部 | 小图标、需要透明 |
| JPEG | 25% 到 35% | 不支持 | 全部 | 照片兜底 |
| WebP | 15% 到 25% | 支持 | 96% 以上 | 主力格式 |
| AVIF | 10% 到 18% | 支持 | 85% 左右 | 面向未来 |
WebP 压缩率比 JPEG 再省三成,又支持透明与动画,2026 年兼容性已经不是问题。我们的策略:产物统一 WebP,AVIF 暂不做(编码慢、CI 时间翻倍不划算),PNG 只保留小尺寸图标。注意渐进式 JPEG 之类的细节不必折腾,目标平台已经统一现代浏览器。更多 CDN 侧的缓存与格式协商策略,参考 4.1 CDN 接入。
2.4.3 sharp 流水线
sharp 是 Node 生态最快的图像库,底层 libvips 是 C++ 实现,不依赖 GraphicsMagick 之类的外部运行时。安装一般一条命令:
npm i -D sharp
构建脚本 scripts/images.ts 的核心:
import sharp from "sharp";
import { readdir, mkdir } from "node:fs/promises";
import { join, extname } from "node:path";
const INPUT = "src/assets";
const OUTPUT = "dist/assets";
const files = (await readdir(INPUT)).filter((f) =>
[".png", ".jpg", ".jpeg"].includes(extname(f).toLowerCase())
);
await mkdir(OUTPUT, { recursive: true });
for (const f of files) {
const name = f.replace(/\.\w+$/, "");
await sharp(join(INPUT, f))
.rotate() // 修正手机拍照 EXIF 朝向
.resize({ width: 1920, withoutEnlargement: true })
.webp({ quality: 78 })
.toFile(join(OUTPUT, `${name}.webp`));
console.log(`${f} -> ${name}.webp`);
}
quality 78 是我们肉眼对比后的甜点值:78 以上体积直线上升但几乎看不出差别,70 以下天空出现色带。withoutEnlargement 防止小图被放大变糊,1920 封顶覆盖主流屏。批量处理 30 张图约 6 秒,已经集成进 2.2 的 npm run build 链路,一次构建全程无人工。
2.4.4 响应式:一张图管三种屏
再输出三档宽度,配合 srcset 让手机不下载 1920 宽的大图:
for (const w of [640, 1080, 1920]) {
await sharp(join(INPUT, f))
.resize({ width: w, withoutEnlargement: true })
.webp({ quality: 78 })
.toFile(join(OUTPUT, `${name}-${w}.webp`));
}
HTML 侧用 picture 标签做格式回退,写在 Vue 组件里:
<picture>
<source type="image/webp" srcset="/assets/hero-1080.webp" />
<img src="/assets/hero-1080.jpg" alt="首屏主视觉" loading="lazy" />
</picture>
除了格式回退,还要给浏览器喂 sizes 提示,避免它在小屏上仍然预取大图。我们的组件约定:首屏大图按视口 100vw 计,博客内文图按容器 680px 计。配错 sizes 的典型症状是 Lighthouse 显示图片明明转了 WebP,移动端流量却没降——浏览器按 sizes 下载了 1920 那一档。排查时用 DevTools 的 Network 面板按宽度筛选请求即可确认。其实整个响应式方案最终都要与 CDN 缓存策略配合,双方各管一段:构建侧出多档文件,CDN 侧做缓存与刷新,这在 CDN 接入与缓存策略 里有完整链路。
有了正确的 sizes 提示,loading=lazy 才能真正发挥作用:首屏外的图延迟加载,博客长页首屏再省 1.2MB。不支持 WebP 的老旧浏览器会自动 fallback 到 img 里的 JPEG 兜底。
2.4.5 踩坑实录
坑一:CI 上 sharp 安装失败。CI 容器架构与本地不同,sharp 预编译二进制偶有缺失。解法是 CI 缓存 node_modules 并固定 sharp 版本,避免它悄悄升级后二进制不匹配。
坑二:图名没变、内容变了,CDN 缓存里还是旧图。后来所有产物图名带内容哈希(如 hero-a3f2.webp),配合 CDN 刷新策略,彻底告别缓存不更新问题。
坑三:EXIF 朝向。手机竖拍照片不调 rotate() 时在页面上横躺,加上 .rotate() 后正常。这个属性在部分工具链里默认不开,sharp 需要显式写。
坑四:SVG 混进流水线。有次 logo 的 SVG 也被脚本扫进去,sharp 对 SVG 的处理需要额外的密度参数,直接报错中断构建。解法是在过滤扩展名时把 SVG 单独排除:SVG 本身是矢量,体积已经很小,需要优化时手工用工具压一遍即可,不值得进自动化链路。
2.4.6 压缩前后对照
把改造前后的关键页面记录下来,作为后续讲性能与成本的证据:
| 页面 | 改造前体积 | 改造后体积 | 降幅 | 备注 |
|---|---|---|---|---|
| 首页 | 5.4MB | 0.9MB | 83% | 首屏图加响应式 |
| 产品页 | 3.1MB | 0.6MB | 81% | 统一转 WebP |
| 博客列表 | 4.7MB | 1.1MB | 77% | 懒加载生效 |
| 关于我们 | 1.2MB | 0.4MB | 67% | 本来图就少 |
这张表钉在 docs/ 里,每次设计交付新切图后跑一次脚本更新数据。有了量化指标,团队对“要不要花时间优化”再没有分歧,流量账单的变化也让老板愿意继续投入体验优化。
2.4.7 效果与小结
一周后复测:首屏图片从 4.2MB 降到 780KB,移动端打开 1.2 秒,CDN 月流量费预期降六成。图片流水线成了 npm run build 的一环,设计交付新图后跑一次命令即可,再也不用去在线压缩网站排队传图。要体验这套构建产物上线后的加载速度,可以直接试腾讯云网站托管的演示:https://cloud.tencent.com/act/cps/redirect?redirect=2497&cps_key=2886aaf7fd441c23ba682c6fda7a9aff&from=console
第二篇到这就把仓库、构建、模板、图片四件事理顺了,dist 目录随时可部署。第三篇我们正式把站传到云上:从 COS 存储桶开通 开始,接域名与 SSL 证书。
延伸阅读
- Node.js 网站托管实战:全书目录,本篇为第二篇收尾。
- 腾讯云实践案例集:更多静态站性能优化与成本案例。
