LCP 优化:从 4s 到 1.5s,6 个手段怎么排序
LCP 优化:把 4s 量级压到 1.5s 量级的 6 个手段排序思路
现象:LCP 4s 报警,但不知道该动哪一行代码
典型表现:PageSpeed Insights 或 Lighthouse 给出 LCP 4.1s、评分红色,报告里是一串通用待办(减少未使用 JS、启用文本压缩、预连接),没有一条指向首屏那个具体元素。用 PerformanceObserver 订阅 largest-contentful-paint,可以打印 entry.element、entry.url 和 entry.startTime(LCP 绘制时间相对 navigation start 的毫秒数),完整四段拆分写法见下文“把 LCP 拆成四段”。示例输出:
2480 <img src="/hero.webp"> https://example.com/hero.webp
拿到元素后再用 Performance 面板看这条 LCP 标记的四段构成:TTFB、资源加载延迟、资源加载时长、元素渲染延迟。
常见误判是把 LCP 慢统一归因为「首屏大图太大」就去压图,压完只降 200ms。瓶颈更常落在 TTFB 大于 800ms,或字体加载阻塞文本渲染。另外,LCP 只统计视口内最大的 img、video poster、CSS 背景图和文本块,首屏之外的瀑布流大图、loading="lazy" 的图片不参与计算,先确认你优化的就是同一个元素。最后,Lighthouse 单次 lab 数据和 CrUX、web-vitals 上报的 field 数据常差 1s 以上,决策看 field 的 p75,lab 只用于复现单次改动效果。
第一步:把 LCP 拆成四段,判断瓶颈在网络、服务器还是渲染
LCP 由四段构成,四段之和就是 LCP 值:
- TTFB:
responseStart - activationStart - 资源加载延迟:LCP 资源开始下载的时刻 − TTFB 结束时刻
- 资源加载时长:下载完成 − 开始下载
- 元素渲染延迟:LCP 绘制时刻 − 资源加载完成
flowchart LR
A["导航开始"] --> B["TTFB"]
B --> C["资源加载延迟: 发现元素并发出请求"]
C --> D["资源加载时长: 下载完成"]
D --> E["元素渲染延迟: LCP 绘制"]
DevTools Performance 面板录制时勾选 Web Vitals 轨道,hover 时间轴上的 LCP 标记,工具会直接给出这四段的秒数拆分和对应的元素节点,比逐条对时间戳快。
判读规则:
| 占主导的段 | 判断方向 |
|---|---|
| TTFB > 800ms | 服务端或 CDN 回源 |
| 资源加载延迟占比最高 | 资源发现太晚:缺 preload / fetchpriority,或被 CSS、JS 挡在后面 |
| 资源加载时长高 | 资源体积或格式问题 |
| 渲染延迟高 | 主线程被 JS、字体或未内联的关键 CSS 阻塞 |
取四段做上报:
import { onLCP } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
const {
element,
url,
timeToFirstByte,
resourceLoadDelay,
resourceLoadDuration,
elementRenderDelay,
} = attribution;
console.log({ value, element, url, timeToFirstByte, resourceLoadDelay, resourceLoadDuration, elementRenderDelay });
});
attribution 里的字段分别对应四段。不要用 LCP entry 的 startTime 反推资源开始下载时刻,它是 LCP 绘制时间,等同 renderTime。
定位 LCP 元素:三种取法互相印证
三种取法互相印证,避免只凭一条信息误判。
Lighthouse 审计条目:报告底部「Largest Contentful Paint element」直接给出 LCP 元素的选择器路径、渲染尺寸和节点外链,点击可在 Elements 面板高亮。适合第一次定位。
DevTools Performance 面板:录一段加载过程,Timings 轨道上的 LCP 标记悬停可看到关联元素,配合主线程活动判断瓶颈是否在渲染。
PerformanceObserver:字段最细,适合放进代码或做长期监控。entry.element 返回真实 DOM 节点,entry.url 返回图片资源地址,entry.size 返回元素面积。entry.url 为空说明 LCP 元素是文本块,此时方向转到 font-display、字体预加载和消除阻塞 CSS,压图无效。完整字段名可复用上文 onLCP 的 attribution 输出;若要在 Console 临时查看,可在页面加载期间用 PerformanceObserver 订阅并打印这些字段。SPA 路由切换后条目可能重置,以首屏那次为准。
四类场景的可复制配置片段
- 首屏大图:给 LCP 图片本身加预加载和高优先级,并写上宽高避免布局抖动,格式换 WebP 或 AVIF。
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<img src="/hero.webp" fetchpriority="high" width="1200" height="600"
decoding="async" alt="">
fetchpriority="high" 只加在 LCP 元素上。首屏有多张图时全部标 high 会互相拉扯,等于没标。
- 字体:预加载首屏用到的那个 woff2 文件,
crossorigin不能省。同源字体同样按 CORS 请求获取,不要因为同源就删掉crossorigin,否则与 @font-face 的请求不匹配、会重复下载。验证方式:Network 面板看字体请求是否只有一次,preload 与 @font-face 的 href、type、crossorigin 是否一致。
<link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>
@font-face {
font-family: main;
src: url("/fonts/main.woff2") format("woff2");
font-display: swap; /* 或 optional,弱网下更稳 */
}
用子集化只保留首屏字符集。别用 @import 引字体样式表,它是串行阻塞。
- 阻塞 CSS:首屏样式内联进
<style>,其余样式表异步加载。
<link rel="preconnect" href="https://img.example.com">
<link rel="stylesheet" href="/app.css" media="print" onload="this.media='all'">
异步加载剩余样式表会带来 FOUC 和 CLS,改动后要用同一节流条件复测首屏是否闪烁、CLS 是否恶化。JS 被禁用或加载失败时页面会一直用打印样式,建议补 <noscript> 兜底。
普通 <img> 图片请求不带 CORS 凭据模式,普通图片预连接不要加 crossorigin;只有确实以 CORS 方式发起的请求(如带 crossorigin 属性的 img、字体、接口)才加:
<link rel="preconnect" href="https://fonts.example.com" crossorigin>
- TTFB:不改服务端架构,只调响应头。静态资源长期缓存,HTML 每次校验。
HTML 响应头(每次使用前校验):
Cache-Control: no-cache
带指纹的静态资源响应头(文件名含哈希,内容变更即变 URL):
Cache-Control: public, max-age=31536000, immutable
文本响应压缩:
Content-Encoding: br
服务端耗时观察:
Server-Timing: db;dur=42, app;dur=88 # 示例值
文本响应开 gzip 或 brotli;Server-Timing 用来确认耗时落在数据库还是应用层,浏览器 Network 面板可直接看到分段。若要在 HTML 上使用 stale-while-revalidate,[待核实:no-cache 与 stale-while-revalidate 组合的实际语义,以及旧版浏览器支持情况]。
手段—适用场景—副作用—收益判断对比,按投入产出排序
| 手段 | 适用场景 | 副作用 | 收益判断(需本地复测确认量级) |
|---|---|---|---|
LCP 图 preload + fetchpriority="high" |
加载延迟段占比高、图片在 CSS/JS 之后才被引用 | 滥用会挤占带宽,拖慢其他首屏资源 | 通常直接缩短资源加载延迟段 |
| 图片压缩 + WebP/AVIF | 加载时长段高、原始图体积大 | 需要回退格式与构建流程改动 | 缩短资源加载时长,幅度取决于原图体积和网络 |
font-display: swap + 字体 preload |
LCP 元素是文本、渲染延迟段高 | swap 带来 FOUT,可能增加 CLS | 缩短文本渲染延迟,需盯 CLS |
| 关键 CSS 内联 + 异步加载剩余样式 | 渲染延迟段高、样式表阻塞首屏 | HTML 体积变大,样式缓存复用变差,异步 CSS 可能 FOUC/CLS | 缩短渲染延迟,效果取决于首屏样式占比 |
| preconnect / dns-prefetch | LCP 资源在第三方域 | 连接的域过多反而增加开销 | 减少第三方域连接开销 |
压缩、缓存头、stale-while-revalidate |
TTFB 段高 | 缓存失效策略配错会返回陈旧内容 | 改善 TTFB,需核实 stale-while-revalidate 语义与兼容性 |
我的排序建议:
- 资源发现类(preload + fetchpriority + preconnect)。改动最小,直接缩短加载延迟段。
- 资源体积类(图片格式与压缩)。收益确定,但要动构建流程。
- 渲染阻塞类(关键 CSS 内联、字体)。收益依赖首屏结构,副作用要盯 CLS。
- TTFB 相关(缓存与压缩头)。验证周期最长,非瓶颈时不要先动。
不要六项全上。如果四段拆分显示 TTFB 只占 200ms,再去优化服务端压缩的收益通常不如把精力放在渲染延迟段。
每改一项单独复测一次。多个变量同时变化,你无法判断收益来自哪一项——比如同时加了 preload 和内联 CSS,加载延迟段和渲染延迟段都会动,归因就断了。
复测方法与适用边界清单
- 改动前后条件一致:Lighthouse 勾选 Slow 4G + 4x CPU 降速,或 Performance 面板用同一套节流参数(Network/CPU 都固定,不要一次只开一项)。两次跑的 LCP 元素、四段拆分都要对齐。
- 先确认 LCP 元素 ID 没变。改动可能把 LCP 从首屏大图换成标题文本,总分变好但优化对象已换,后面的结论会失真。
- 用 web-vitals attribution 打四段,或看 Performance 面板的 Web Vitals 轨道,不要只看总分。不要用 LCP entry 的
startTime反推资源开始下载时刻:它是 LCP 绘制时间,等同renderTime,用它算resourceDelay/resourceLoad会得到错误结果。四段字段见上文onLCPattribution。 - 跨域图片的 renderTime 可能为 0,需要对方返回
Timing-Allow-Origin[待核实:当前浏览器行为]。 - Field 验证:CrUX 和 Search Console 的 LCP 是 28 天滚动窗口,改动当天看不到变化。短期用 lab 数据判断方向,长期用 web-vitals 自建上报看 p75 趋势。
- 适用边界:本篇只覆盖浏览器渲染路径和资源优先级,不动服务端架构、不做 CDN 回源改造。SPA 路由切换后的 LCP、INP、CLS 是独立指标,各有各的路径。TTFB 若来自数据库慢查询或后端限流,加响应头和缓存只能缓解。
- 1.5s 成立前提:桌面端或良好 4G 下,四段预算可参考下表;这是预算,不是实测值,需按 field p75 调整。
| 阶段 | 预算 |
|---|---|
| TTFB | ≤ 300ms |
| 资源加载延迟 | ≤ 200ms |
| 资源加载时长 | ≤ 500ms |
| 元素渲染延迟 | ≤ 500ms |
| 合计 | ≤ 1500ms |
若 TTFB 明显超预算,先查服务端;若元素渲染延迟超预算,先处理字体和阻塞 CSS。