原生懒加载只要一个 loading="lazy" 属性,不必引库,也不用自己监听滚动。于是不少团队干脆在封装的 <Image> 组件里把它设成默认,或者让构建插件给每个 <img> 自动补上。下面的商品列表确实省了流量,可首屏那张大 banner 也跟着被懒加载了。这时候再跑 Lighthouse,LCP 往往比改之前还难看。
慢在哪一步
浏览器拿到 HTML 后,预加载扫描器会抢先找出文档里的图片地址发请求,这时候页面还没开始排版。一旦图片带上 loading="lazy",浏览器就得等布局算完、确认它离视口足够近,才真正发起请求。web.dev 那篇讲优化 LCP 的文章专门拿这种写法当反例。
那篇文章把 LCP 拆成四段,DevTools 和 Lighthouse 里都能看到:
- TTFB 指从发起导航到收到 HTML 第一个字节的时间。
- 资源加载延迟指 TTFB 之后到 LCP 图开始下载的这段空档。
- 资源加载耗时就是下载本身花掉的时间。
- 元素渲染延迟指下载完成后到元素真正画出来的间隔。
误用 lazy 拖长的是第二段。图片体积和网络都没变,多出来的时间全耗在迟迟不开始下载上。按 web.dev 给的参考比例,资源加载延迟和元素渲染延迟这两段,各自都该压在 LCP 的 10% 以内。LCP 想进 good 区间,得让至少 75% 的访问在 2.5 秒内完成。
用 JS 库懒加载、把地址藏进 data-src 的老写法也是同一个毛病,扫描器根本看不到真实的 src。至于拿 scroll 事件加防抖自己实现懒加载,那是另一类问题,这里不展开。
怎么改
思路是首屏和非首屏分开处理:
<!-- 首屏 banner:不带 loading,提高优先级,宽高写死防 CLS -->
<img src="/img/banner-1600.webp"
width="1600" height="600"
fetchpriority="high"
alt="秋季活动">
<!-- 首屏以下的商品图:照常懒加载 -->
<img src="/img/item-01.webp" loading="lazy"
width="400" height="400" alt="商品 1">
改的时候还有几处要注意:
- 首屏 LCP 图去掉
loading="lazy",浏览器默认就是 eager。工具链要是会自动补属性,就显式声明loading="eager",免得被改回去。 fetchpriority="high"只留给一两张最可能成为 LCP 的图。web.dev 提醒过,给太多图片设高优先级,这个提示就失去意义了。fetchpriority="high"跟loading="lazy"叠在一起意义不大:图在屏幕外时照样被推迟,靠近视口后才以高优先级下载。- banner 如果是 CSS 背景图,扫描器发现不了它,得在
<head>里加一条 preload,这个标签本身同样要带fetchpriority="high"。 - 轮播里一开始看不见的几张,可以设
fetchpriority="low",把带宽让给第一张。 - 每张图都写上
width和height。懒加载图没有尺寸时按 0×0 算,浏览器可能认为一整排都在视口内,结果一次性全拉下来。尺寸缺失还会让晚到的图片把内容顶开,CLS 跟着变差。
背景图的 preload 大概长这样:
<link rel="preload" as="image" href="/img/banner-1600.webp" fetchpriority="high">
属性取值可以对照 MDN 的 img 页面,懒加载的行为细节在 web.dev 的 Browser-level image lazy loading 里讲得比较全。
哪张才是 LCP
改之前得确认 LCP 元素是谁,有时候它是一段标题文字,根本不是那张图。
- Chrome DevTools 的 Performance 面板打开就有 live metrics,会显示本地测到的 LCP 和对应节点。录一段 trace 后,Insights 里的 LCP breakdown 会列出四段各占多少。
- Lighthouse 13 删掉了旧的
lcp-lazy-loaded和prioritize-lcp-image审计。取代它们的 insight 叫 LCP request discovery。JSON 报告里对应的 id 是lcp-discovery-insight。它检查三件事:图能不能从 HTML 里直接发现、有没有被懒加载、带没带fetchpriority=high。 - 提醒推迟屏外图片的
offscreen-images审计也在 13 版移除了。官方给的理由是浏览器本来就会降低屏外图片的优先级,懒加载能省带宽,对 Lighthouse 测的指标影响却不大。
控制台里也能直接打出来:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log(last.element, last.url, last.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
最后一条记录就是当前的 LCP 候选,url 为空的话,多半是文字。
你们的图片组件默认走哪边:全量 lazy 再把首屏排除掉,还是反过来只在列表里开启?