同一张插画,原始 JPEG 有 645KB。优化之后,手机 1x 屏的 640 档只需要 43KB,2x 屏自动选择 960 档约 65KB。
这是一次很小的性能优化,但它几乎涵盖了我对“前端渲染优化”的全部理解:不要让浏览器下载它不需要的东西,也不要让它等待它马上要用的东西。
先看结果
| 版本 | 尺寸 | 体积 | 相对原图 |
|---|---|---|---|
| 原始 JPEG | 969 × 969 | 645 KB | 100% |
| JPEG(渐进式) | 1280 × 1280 | 249 KB | 38.6% |
| WebP | 1280 × 1280 | 122 KB | 18.9% |
| AVIF | 1280 × 1280 | 79 KB | 12.1% |
| AVIF | 960 × 960 | 64 KB | 9.9% |
| AVIF | 640 × 640 | 43 KB | 6.7% |
首屏主图在桌面端使用 960 或 1280 的版本,手机端在 1x 屏使用 640、2x 屏自动使用 960。也就是说,最常见的情况下,这张图的传输量只有原来的 7%–10%。
第一步:不要再发一张 969 的图给所有屏幕
原图是正方形,但它在页面里从来不会以 969px 宽显示。桌面端它被放在右侧,约占视口宽度的 55%;手机端则铺满整个屏幕,但屏幕的物理宽度通常只有 360–430px。
所以第一步是生成尺寸阶梯:
# 伪代码:保持比例,生成三档宽度
for width in 640 960 1280:
resize(source, width) -> hero-{width}.jpg
1280 这一档其实是从 969 放大得到的。对插画来说,轻微放大不会明显损失观感,反而能让大屏上的边缘更干净。如果原图更小,就不应该继续放大,直接用原生尺寸作为上限。
第二步:格式比参数更重要
在同一档尺寸下,我分别导出了 JPEG、WebP 和 AVIF:
- JPEG 使用渐进式加载,兼容性最好,作为最后的兜底。
- WebP 在 80 左右的 quality 下,体积大约是 JPEG 的一半。
- AVIF 的压缩率更高,但编码更慢,适合构建时离线生成。
浏览器自己会选择最合适的格式:
<picture>
<source
type="image/avif"
srcset="/assets/img/hero-640.avif 640w,
/assets/img/hero-960.avif 960w,
/assets/img/hero-1280.avif 1280w"
sizes="(max-width: 900px) 100vw, 55vw">
<source
type="image/webp"
srcset="/assets/img/hero-640.webp 640w,
/assets/img/hero-960.webp 960w,
/assets/img/hero-1280.webp 1280w"
sizes="(max-width: 900px) 100vw, 55vw">
<img
src="/assets/img/hero-960.jpg"
width="969" height="969"
alt=""
fetchpriority="high"
decoding="async">
</picture>
几个容易忽略的细节:
sizes必须和真实布局一致。写错的话,浏览器会选错档位,优化就白做了。- 给
<img>写明width和height,避免布局偏移。 - 首屏图片使用
fetchpriority="high",但不要给所有图片都加,否则等于没加。 - 这张图是装饰性的,
alt=""比堆一段关键词更合适。
第三步:让浏览器更早发现它
图片在 HTML 里出现得比较晚,浏览器要解析到 <picture> 才知道要下载。用 <link rel="preload"> 可以提前告诉它:
<link
rel="preload"
as="image"
type="image/avif"
imagesrcset="/assets/img/hero-640.avif 640w,
/assets/img/hero-960.avif 960w,
/assets/img/hero-1280.avif 1280w"
imagesizes="(max-width: 900px) 100vw, 55vw">
只预加载 AVIF。不支持 AVIF 的浏览器会忽略这个 type,继续走 <picture> 的 WebP 或 JPEG,不会产生双重下载。
第四步:把装饰交给 CSS
如果背景里还需要网格、扫描线、紫色光晕、噪点,不要分别导出图片。它们都可以用 CSS 渐变完成:
.hero__media::after {
background:
linear-gradient(90deg, #07060a 0%, rgba(7, 6, 10, .6) 32%, transparent 62%),
radial-gradient(60% 80% at 80% 20%, rgba(168, 85, 247, .28), transparent 70%),
repeating-linear-gradient(0deg, transparent 0 3px, rgba(255, 255, 255, .025) 3px 4px);
}
CSS 渐变没有额外请求,压缩后只有几百字节,而且在任何分辨率下都清晰。扫描线使用 repeating-linear-gradient 比平铺 PNG 更省资源。
第五步:服务端也要配合
图片变小了,但如果服务器每次都重新压缩、或者让浏览器反复验证缓存,体验仍然不好。Nginx 这边做了三件事:
gzip on;
gzip_static on;
gzip_types text/css application/javascript application/json image/svg+xml;
location ^~ /assets/ {
expires 1y;
access_log off;
}
gzip_static 会优先发送构建时预压缩好的 .gz 文件,省掉运行时的压缩开销。带哈希或版本号的静态资源可以放心缓存一年;HTML 则保持协商缓存,保证更新能及时看到。
最后看数字
在 Chrome 的 Lighthouse 里,这张图不再是 LCP 的瓶颈。真正的提升不只来自“图片变小”,而是几件事叠加:
- 正确的尺寸:不把桌面图发给手机。
- 正确的格式:AVIF 优先,WebP 兜底。
- 正确的优先级:首屏图 preload,非首屏图 lazy。
- 正确的缓存:静态资源一年,HTML 不缓存。
- 正确的实现:装饰用 CSS,而不是更多图片。
优化不是把一张图压到最小,而是让每个屏幕只下载它真正需要的那一张。
一张图的最佳体积,不是某个固定的 KB 数,而是“在当前屏幕上刚好够用”。