🔍 登录 注册

图像优化链路 AVIF 协商、响应式图片与 LCP 改善

今 今晚打老虎 2026-10-23 👁 0 ⏱ 6min

图像优化链路 AVIF 协商、响应式图片与 LCP 改善

图片通常占页面字节数一半以上,也是 LCP 的主要构成。但多数站点的图片优化停留在"压一下质量"。真正的收益来自一整条链路:格式协商、尺寸协商、加载优先级、以及缓存与 CDN 的配合。本文按请求路径逐段处理。

一、格式协商:AVIF/WebP 的正确做法

不要只发一种新格式——Safari 老版本与部分内嵌浏览器不支持 AVIF,硬发会得到破图。正确做法是协商:

服务端协商(最省,无额外字节):

function pickFormat(accept = "") {
  if (accept.includes("image/avif")) return "avif";
  if (accept.includes("image/webp")) return "webp";
  return "jpeg";
}

app.get("/img/*", (req, res, next) => {
  const fmt = pickFormat(req.get("accept"));
  req.params[0] = req.params[0].replace(/\.(jpe?g|png|webp|avif)$/i, `.${fmt}`);
  res.set("vary", "Accept");           // 必须,否则 CDN 会把 AVIF 发给不支持的客户端
  next();
});

vary: Accept 是关键,漏掉会造成 CDN 缓存串味(把 AVIF 缓给不支持的 UA)。若不希望引入 Vary,改用 HTML 的 <picture> 由浏览器自选。

声明式(推荐用于 LCP 图):

<picture>
  <source type="image/avif" srcset="/hero-1200.avif 1200w, /hero-800.avif 800w" sizes="(max-width:800px) 100vw, 800px">
  <source type="image/webp" srcset="/hero-1200.webp 1200w, /hero-800.webp 800w" sizes="(max-width:800px) 100vw, 800px">
  <img src="/hero-1200.jpg" width="1200" height="675" alt="封面" fetchpriority="high" decoding="async">
</picture>

同一 type 下多个 <source> 只会被选第一个匹配的(不看 media),所以 media 条件要写在 <source> 上,格式协商用 type。

二、尺寸协商:srcset 与 DPR

浏览器真正需要的是"显示像素 = CSS 像素 × DPR"。给 375px 宽的容器发 1200px 图是纯浪费。

<img src="/cover-800.avif"
     srcset="/cover-400.avif 400w, /cover-800.avif 800w, /cover-1600.avif 1600w"
     sizes="(max-width: 640px) 100vw, (max-width: 1080px) 50vw, 400px"
     width="800" height="450" alt="">

w 描述符配合 sizes 是主流写法,浏览器自己按 DPR 与布局算出该取哪个。用 x 描述符只处理 DPR,不处理布局宽度——响应式布局下基本不该用。

尺寸档位别贪多。经验上是 3~4 档(400/800/1600 或 320/768/1536)。档数翻倍意味着 CDN 边缘缓存命中率骤降与生成成本翻倍。

三、生成链路:一次构建而非每次请求

实时用 libvips 转码看着优雅,实则 CPU 峰值与冷启动很慢。批量预生成更稳:

// scripts/build-images.mjs
import sharp from "sharp";

const WIDTHS = [400, 800, 1600];
for (const file of inputs) {
  const base = sharp(file, { failOn: "none" }).rotate();     // rotate() 处理 EXIF 方向
  for (const w of WIDTHS) {
    await base.clone().resize({ width: w, withoutEnlargement: true })
      .webp({ quality: 78, effort: 6 }).toFile(`${out}/${stem}-${w}.webp`);
    await base.clone().resize({ width: w, withoutEnlargement: true })
      .avif({ quality: 50, effort: 6 }).toFile(`${out}/${stem}-${w}.avif`);
  }
}

经验参数:WebP q=78、AVIF q=50(AVIF 在同等质量下可再省 15~30%);effort 构建期开高、生成期开低。

必须去掉 EXIF 中的敏感信息(GPS、设备序列号):.withMetadata({ redundancy: 1 }) 显式控制,默认 sharp 已不带 metadata,但要确认没走 .toBuffer() 前保留路径。

四、LCP 的四个决定性因素

LCP 元素九成是首屏大图。四个必做项:

  1. fetchpriority="high" + 不懒加载:loading="lazy" 加在 LCP 图上会让指标恶化一个数量级;
  2. width/height 必写:避免 CLS,同时浏览器能提前算布局;
  3. preload 带完整协商:
<link rel="preload" as="image" imagesrcset="/hero-1200.avif" imagesizes="100vw" fetchpriority="high">
  1. CDN 边缘命中 + 长缓存:
Cache-Control: public, max-age=31536000, immutable

文件名要含内容 hash 或版本段,否则改图无法刷新。

验证手段固定用 Lighthouse + RUM 双通道:Lab 数据看趋势与回归,Field 数据(CrUX)看真实 P75。

2xx 缓存命中率的 CDN 指标 + LCP P75  这两个数一起看才有效

五、非首屏与占位

首屏以下正确懒加载,但要避免布局跳动与"白块闪烁":

<img loading="lazy" decoding="async" width="800" height="450"
     style="aspect-ratio: 16/9; background: #f1f5f9" src="/cover-800.avif" alt="">

aspect-ratio + 背景色是最省事的占位方案,比 LQIP 内联 base64 更省字节。需要更精致时再上低质量占位(把图缩到 16px 宽、blur、内联 data URI),但注意内联 base64 会挤占 HTML 体积,列表页谨慎使用。

六、别忘了安全与回退

两处必须处理:

  1. 图片路径穿越:/img/../../etc/passwd —— 拼路径前必须规范化 + 白名单校验目录,只允许 [A-Za-z0-9._-];
  2. 解码炸弹:超大尺寸 PNG(几十万像素但解压后 GB 级)会打爆内存。上传侧用 sharp().metadata() 先校验宽高与字节上限。
const meta = await sharp(buf, { failOn: "none" }).metadata();
if (meta.width * meta.height > 40_000_000) reject("image too large");

回退兜底:<img> 始终留在 <picture> 最后且带原生格式 src,别把所有 <source> 之外的兜底图也写成 AVIF。

小结

图片优化是一条链路而不是一个开关:**Accept 协商(或 <picture>)**决定格式,srcset + sizes决定尺寸,预生成 3~4 档决定成本,fetchpriority + 不懒加载 + width/height + preload决定 LCP,immutable + 文件名 hash决定命中。最后加上路径校验与解码上限保证安全。按这条链路逐项做完,列表页与详情页的 LCP 与总字节通常能降到原来的三分之一左右,而且是可持续维护的收益。

读完了?给作者一点支持

本文采用 CC BY 4.0 协议转载 · © 今晚打老虎

评论(0)

登录 后参与评论。

  • 还没有评论,来抢沙发。

🔗 相关文章