文章里一张大图,处理不好就是 LCP 杀手 + CLS 元凶。好消息是:绝大多数收益靠几行“该加的加上、该设的设对”就能拿到。
基线三件套
<img
src="/img/diagram.png"
alt="架构示意图"
width="1200"
height="800"
loading="lazy"
decoding="async"
/>
width/height(或 CSSaspect-ratio)是防 CLS 的第一功臣——浏览器据比例预留占位空间,图片加载完成不撑动版面。缺尺寸的图是内容站最常见的布局偏移来源。loading="lazy":离视口远的图不请求。首屏 LCP 图千万别加——懒加载会延迟它自己的加载,把最大内容元素推后。判断标准:位于首屏折叠线以上的第一张图,用loading="eager"甚至fetchpriority="high"。decoding="async":解码移出主线程,避免大图解码卡顿交互。
CSS 侧兜住尺寸
属性宽高会被样式覆盖,用 CSS 强制保持比例、限制不溢出容器:
.article-content img {
max-width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height); /* 现代浏览器据属性自动推比例,这行作显式保险 */
}
实际现代浏览器已由 width/height 属性自动推 Aspect Ratio,CSS 这行更多是“确保不被某处 height: auto 之外的规则破坏”。
淡入:锦上添花但要处理边界
想给图片加“加载完成淡入”,务必处理三种“到不了 loaded”的情况,否则图会永久隐身:
function enhance(img) {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; // 直接可见
if (img.complete && img.naturalWidth) return; // 已缓存:不淡入,直接显示
img.dataset.reveal = 'pending';
const reveal = () => {
img.dataset.reveal = 'done';
cleanup();
};
const fail = () => {
img.dataset.reveal = 'done';
cleanup();
}; // 加载失败也要可见(别留隐藏态)
const cleanup = () => {
img.removeEventListener('load', reveal);
img.removeEventListener('error', fail);
};
img.addEventListener('load', reveal);
img.addEventListener('error', fail);
}
[data-reveal='pending'] {
opacity: 0;
}
[data-reveal='done'] {
opacity: 1;
transition: opacity 400ms ease;
}
三个边界对应三条教训:
reduced-motion用户跳过淡入,直接可见;- 命中缓存的图
complete=true,load事件不会再触发——不判断就会卡在 pending 永久透明; - 加载失败也必须解除隐藏——否则一张 404 图变成一块永远空白的隐形坑(比显示破图标更糟)。
客户端路由下还要在换页时移除这些 load/error 监听,避免泄漏。
响应式与格式
- 需要真响应式尺寸时用
srcset/sizes,让浏览器按 DPR 与视口挑选; - 截图/图表类优先 WebP/AVIF(带
type的<picture>或srcset回退); - 装饰性图
alt=""(不是省略 alt),信息性图 alt 描述内容而非文件名。
小结
图片优化的收益顺序很明确:设尺寸防 CLS > 别懒加载 LCP 图 > 异步解码 > 现代格式 > 淡入动效。前四条是纯工程正确,最后一条是体验糖——而糖必须把“缓存/失败/减弱动效”三个苦口处理掉,才不会变成新 bug。