给自己做的柴烧陶艺概念站(Next.js 静态导出,手写 CSS)里,商品图原本都是相机直出:长边 4000px,单张 1.2MB。首页放四张,光图片就 4.8MB。
这篇讲图片管线怎么做,不谈框架。做完的结果:同一张图 58KB,首屏总量 288KB。
一、先按容器宽度定尺寸,再谈压缩
商品图在页面上的容器宽度是 640px(桌面两列),手机是满宽 390px。
拿 4000px 的原图去填 640px 的框,浏览器会把整张下载下来再用 GPU 缩------流量一分没省,还多了一次解码。
所以尺寸从容器倒推:
容器 640px → 出 960w(留 1.5 倍给 2x 屏)
容器 100vw → 出 480w / 960w / 1440w 三档
二、转 WebP,质量从 65 起
同一画面,WebP 在质量 65 时的体积通常是 JPEG 80 的三到五成,肉眼几乎无差。
用 sharp 批量出图:
js
import sharp from "sharp";
for (const w of [480, 960, 1440]) {
await sharp(src)
.resize({ width: w, withoutEnlargement: true })
.webp({ quality: 65 })
.toFile(`out/piece-${w}.webp`);
}
```
判断质量够不够,不看数字看边缘:陶器的高光、织物纹理放大到 200%,出现块状或糊边就回 70;看不出来就试 60。
## 三、srcset + sizes,让浏览器自己挑
```html
<img
src="piece-960.webp"
srcset="piece-480.webp 480w,
piece-960.webp 960w,
piece-1440.webp 1440w"
sizes="(max-width: 720px) 100vw, 640px"
width="960" height="1200"
loading="lazy" decoding="async"
alt="柴烧茶碗"
>
```
两个细节:
- `sizes` 是告诉浏览器「这图会有多宽」,它据此算 DPR 再选文件。写错了(比如永远 100vw)就等于白配。
- `width` / `height` 必须写,否则图片加载完成会把下面的内容顶下去,CLS 直接爆。
## 四、验收:三个数
改完不看分数,看三个数:
1. 首屏 transferred 总量(DevTools 勾 Disable cache)
2. 最大那张图的体积(通常是首屏 banner)
3. 有没有 CSS 缩图(`width:100%` 把 4000px 缩成 800px 的情况)
我这里的记录:
| 项 | 改前 | 改后 |
| --- | --- | --- |
| 单张商品图 | 1,240KB | 58KB |
| 首页总量 | 5.1MB | 288KB |
| 目录页 | 2.4MB | 191KB |
| CLS | 0.24 | 0 |
## 五、几个坑
- 静态导出 + 子路径部署时,`srcset` 里的相对路径不会自动加前缀,构建后要统一替换,否则全部 404。
- 原图别删,留给「点击放大」;但别把原图塞进 `<a href>` 让爬虫预取。
- 首屏第一张图不要 `loading=lazy`,会导致 LCP 变差;后面的才懒加载。
- WebP 兼容性现在够用(Safari 14+),真要兜底就上 `<picture>` + AVIF/JPEG。
## 取舍
这套管线是构建期跑脚本,不是运行时图片服务。好处是零依赖、可静态托管;坏处是新增图片要重新构建。图片量大的电商站还是上 CDN 的图片处理(自动裁切 + 格式协商),这里只是把「不改代码就能变快」的那部分先吃掉。
站是自建的演示项目,品牌和数据都是虚构的。