TECH · 3 MIN READ · 1669 字

一张 645KB 的背景图,如何变成首屏 65KB

同一张插画,原始 JPEG 有 645KB。经过尺寸分级、格式替换和加载优先级调整后,手机 1x 屏首屏最低只需 43KB,2x 屏约 65KB。

同一张插画,原始 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>

几个容易忽略的细节:

  1. sizes 必须和真实布局一致。写错的话,浏览器会选错档位,优化就白做了。
  2. 给 <img> 写明 width 和 height,避免布局偏移。
  3. 首屏图片使用 fetchpriority="high",但不要给所有图片都加,否则等于没加。
  4. 这张图是装饰性的,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 数,而是“在当前屏幕上刚好够用”。