- LCP 的 "Good" 阈值是 2.5 秒(p75,移动端),Chrome 用户体验报告(CrUX)以此为准。
fetchpriority="high" 从 2024 年起进入 Baseline,可以让 LCP 图片的下载优先级从 Low 提升到 High,实测 LCP 平均下降 10%–15%。
<link rel="preload"> 主要用来在 HTML 解析器发现资源之前就发起下载,最适合被 CSS background-image 引用的 LCP 图片和自定义字体。
- Priority Hints、Early Hints(103)、Speculation Rules 三者是互补关系,不是替代关系:Early Hints 抢 TTFB,Priority Hints 排序下载队列,Speculation Rules 让下一次导航接近 0 毫秒。
- 用
PerformanceObserver 观察 largest-contentful-paint 条目,可以直接在生产环境定位到 LCP 元素的具体 DOM 节点和候选变化过程。
- 永远不要给非 LCP 图片加
fetchpriority="high",那会挤占 LCP 图片的带宽,让指标反而恶化。
LCP 多少才算好?2026 年的阈值和分布
根据 web.dev 上 Google 官方对 LCP 的定义,"Good" 的阈值是 p75 LCP ≤ 2.5 秒,"Needs improvement" 是 2.5–4.0 秒,超过 4.0 秒算 "Poor"。这里的 p75 指的是同一 URL 在过去 28 天里所有真实用户会话的 75 分位值。也就是说,四分之一的用户体验必须比这个数字更快。
2026 年的现实是:移动端普遍比桌面端慢 40%–60%。根据 HTTP Archive 2026 年 Q2 的公开数据,全球移动端 LCP 中位数在 3.1 秒左右,只有约 45% 的原点达到 "Good"。这个数字其实挺反直觉的(4G 已经普及、手机 CPU 越来越强),但页面重量涨得更快。2026 年首屏 JavaScript 中位数已经达到 620 KB(压缩后),比 2020 年翻了一倍。
LCP 由四个子阶段组成,理解这四段是所有优化的前提:
- TTFB(Time to First Byte):从导航开始到收到第一个字节。服务器、CDN、DNS 都在这一段。
- 资源加载延迟:从 TTFB 到 LCP 资源(图片、字体、视频海报)真正开始下载的时间。这一段最容易被忽视,也是
fetchpriority 和 preload 主要攻击的目标。
- 资源加载时长:LCP 资源本身的下载耗时。图片格式、压缩率、CDN 都在这一段起作用。
- 元素渲染延迟:资源到齐后到实际绘制的时间。这一段和主线程繁忙程度、渲染阻塞 CSS 相关。
在优化之前,先用 Chrome DevTools Performance 面板打开 "Insights"(2025 年从 Lightning Bug 迁移过来的新工具),它会自动把这四段的时间列出来,一眼就能看到瓶颈在哪一段。
如何找到你的 LCP 元素?DevTools 三步定位法
优化 LCP 的第一步永远是"知道 LCP 是谁"。说真的,我遇到的团队里,90% 第一次做 LCP 优化都会认错元素。比如以为是 Hero 图片,实际上是页面中间某个大段落。三步定位法:
方法一:Chrome DevTools Performance Insights(推荐)。打开 DevTools → Performance → 点击 Record → 刷新页面 → 停止录制。在 Insights 面板会看到 "LCP by phase" 的分解,点进去可以直接高亮 DOM 节点。这是 2025 年之后最快的定位方式。
方法二:PerformanceObserver 脚本。在生产环境或测试环境直接跑这段代码,会把每一次 LCP 候选打印到控制台:
// 观察所有 LCP 候选,包括中途被更大元素替换的情况
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.element 是当前 LCP 候选的 DOM 引用
console.log('[LCP candidate]', {
time: entry.startTime.toFixed(0) + 'ms',
size: entry.size,
element: entry.element,
url: entry.url || '(text node)',
renderTime: entry.renderTime,
loadTime: entry.loadTime,
});
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
关键点:LCP 是"竞选式"指标。浏览器会持续汇报候选,最终选定的是页面变化停止(用户交互或 5 秒空闲)时最大的那一个。所以你在控制台会看到多条日志,最后一条才是最终的 LCP。
方法三:PageSpeed Insights + CrUX。适合验证真实用户数据。PSI 会同时给你 Lab(Lighthouse 模拟)和 Field(CrUX 真实用户)两组指标,Field 数据下的 "Largest Contentful Paint element" 是权威答案。
fetchpriority="high" 实战:图片、脚本、字体分别怎么用
fetchpriority 是一个 HTML 属性(不是 CSS),2024 年正式进入 Baseline,Chrome、Edge、Safari 16.4+、Firefox 132+ 全部支持。它告诉浏览器:这个资源在同优先级队列里给我提前排,把默认的 Low 提升到 High,或者把默认的 High 降到 Low。
浏览器有一套隐式优先级:CSS 是 Highest,同步 <script> 是 High,视口内图片是 Low,视口外图片是 Lowest。问题在于,LCP 图片通常在视口内,但浏览器要等到 layout 完成才知道它在视口内,这中间会浪费 200 到 800 毫秒。fetchpriority="high" 就是把这个"等 layout"的过程直接跳过。
场景一:LCP 图片(最常见)
<!-- 直接在 img 上加 fetchpriority -->
<img
src="/hero.avif"
alt="产品主图"
width="1200"
height="630"
fetchpriority="high"
decoding="async"
/>
注意三件事:fetchpriority 只加在 确定是 LCP 的那一张图片上;配合 width/height 属性(防 CLS);decoding="async" 让图片解码不阻塞主线程。永远不要给轮播图里所有幻灯片都加 fetchpriority="high",那等于没加。(我在一个新闻站踩过这个坑,加了以后 LCP 反而涨了 400 毫秒。)
场景二:关键脚本降级。有时你希望某个 async 脚本晚一点跑(比如分析 SDK),可以显式降级:
<script src="/analytics.js" async fetchpriority="low"></script>
场景三:fetch() 请求。JavaScript fetch API 也支持 priority 参数:
// 关键数据高优先级
const res = await fetch('/api/product/hero', { priority: 'high' });
// 非关键 prefetch 低优先级
fetch('/api/related-products', { priority: 'low' });
MDN 上有 fetchpriority 属性的完整浏览器兼容表和使用示例,可以作为兼容性查询。
preload 和 fetchpriority 有什么区别?如何组合使用
这是 2026 年被问得最多的问题之一。简短答案:preload 让浏览器提前发现资源,fetchpriority 让浏览器提前下载资源。两者解决不同阶段的问题。
| 维度 | <link rel="preload"> | fetchpriority="high" |
| 解决的问题 | 浏览器发现资源太晚 | 浏览器发现了但排队靠后 |
| 典型用例 | CSS 里的 background-image、字体 | HTML 里已有 <img> 的 LCP 图片 |
| 放置位置 | <head> 里的 <link> 元素 | 目标资源自身的属性 |
| 是否会重复下载 | 是(如果 as 或 type 写错) | 否 |
| Baseline 状态 | 2015 起广泛支持 | 2024 年进入 Baseline |
| 能否组合使用 | 可以,且推荐。preload 也支持 fetchpriority 属性 |
组合示例:假设你的 LCP 图片是 CSS background-image(浏览器只能在解析完 CSS 后才知道要下载),最佳写法是:
<!-- 在 <head> 里 -->
<link
rel="preload"
as="image"
href="/hero.avif"
type="image/avif"
fetchpriority="high"
imagesrcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
imagesizes="100vw"
/>
关键属性:as="image" 必填(否则浏览器不会真去下载);type 让不支持 AVIF 的浏览器跳过;imagesrcset 加 imagesizes 让 preload 也能挑正确的响应式变体(2022 年后 Chrome/Firefox 都支持)。
响应式图片 + LCP:srcset、sizes、AVIF 的正确写法
2026 年 AVIF 已经在 Baseline 里稳定超过 3 年(Safari 16 起支持),全球覆盖率超过 96%。同一张图 AVIF 通常比 JPEG 小 50%,比 WebP 小 20%,直接砍掉一半资源加载时长。这是 2026 年最简单也最有效的 LCP 优化。
完整的 LCP 图片写法应该同时满足:正确的响应式变体、AVIF 优先带 WebP/JPEG 回退、fetchpriority、明确的尺寸防 CLS:
<picture>
<source
type="image/avif"
srcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1600.avif 1600w"
sizes="(max-width: 640px) 100vw, 800px"
/>
<source
type="image/webp"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="(max-width: 640px) 100vw, 800px"
/>
<img
src="/hero-800.jpg"
alt="产品主图"
width="1600"
height="900"
fetchpriority="high"
decoding="async"
/>
</picture>
关键点:width 和 height 一定要写在最后的 <img> 上(不是 source 上),浏览器会用它算 aspect-ratio 防止 CLS;sizes 属性描述图片显示宽度,不是文件宽度;fetchpriority="high" 只写在 fallback img 上就够了,会向上继承到 source。
不要用 loading="lazy":LCP 元素在视口内,加 lazy 会让浏览器等到 layout 完成才发起下载,直接毁掉 LCP。loading="lazy" 只该给折叠线以下的图片用。这一点如果你还在做 INP 优化实战指南,会发现两者的原则是一致的:分辨关键路径和非关键路径,把资源精确分类。
字体是 LCP 时怎么办?preload + font-display 组合
如果你的 LCP 元素是一大段文字(比如新闻站首屏标题),LCP 可能被自定义字体加载卡住。字体默认在被 CSS 引用后才开始下载,从解析 CSS 到发现字体需要 100 到 500 毫秒。这里的正确姿势是三件套:
<!-- 1. 在 <head> 里 preload 字体 -->
<link
rel="preload"
href="/fonts/Inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
fetchpriority="high"
/>
<style>
/* 2. 在 @font-face 里加 font-display: swap */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-var.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
/* 3. 只声明实际用到的 unicode 子集 */
unicode-range: U+0020-007F, U+4E00-9FFF;
}
</style>
三个坑:crossorigin 必须写(即使字体是同源的),否则 preload 会被丢弃;font-display: swap 让浏览器先用系统字体渲染、字体到齐后再切换(避免 FOIT);如果只用少量 unicode,用 unicode-range 分片能把字体从 200 KB 降到 30 KB。
如果 swap 之后的字体切换让你难受(会引起小幅度 layout shift),可以改用 font-display: optional。如果字体在 100 毫秒内没到齐就永远不切换,这是文本 LCP 优化的老手技巧。
降低 TTFB:Early Hints(103)和边缘缓存
如果你的 TTFB 超过 800 毫秒,前面所有优化都是杯水车薪,LCP 上限被服务器锁死了。2026 年降 TTFB 的两个大招是 Early Hints (HTTP 103) 和 边缘 SSR。
Early Hints 是 HTTP/2 引入的中间响应状态码,服务器可以在处理主请求(比如查数据库)的同时,先把要 preload 的资源清单发给浏览器。浏览器收到 103 后立刻开始下载资源,等主 HTML 到达时资源可能已经下载完成。Chrome 103+、Cloudflare、Fastly、Vercel Edge Network 都支持,具体规范可以看 RFC 8297 关于 103 Early Hints 的定义。
// Node.js / Express 示例
app.get('/', (req, res) => {
// 先发 103 Early Hints
res.writeEarlyHints({
link: [
'</hero.avif>; rel=preload; as=image; fetchpriority=high',
'</fonts/Inter-var.woff2>; rel=preload; as=font; crossorigin',
'<https://api.example.com>; rel=preconnect',
],
});
// 然后正常处理主请求(可能耗时几百毫秒)
const data = await slowDatabaseQuery();
res.send(renderTemplate(data));
});
实测数据:Shopify 2024 年公开的实验显示 Early Hints 平均把 LCP 降低 200 毫秒,对于 TTFB 大于 500ms 的页面效果最明显。
如果你的站点已经在做 SPA 之间的丝滑跳转,可以参考 Speculation Rules API 预渲染完全指南,它和 Early Hints 是完全互补的:Early Hints 优化首次进入的 LCP,Speculation Rules 让后续导航接近 0 毫秒。
用 PerformanceObserver 在生产环境监控 LCP
做完优化,必须监控真实用户的 LCP。Lab 数据(Lighthouse)和 Field 数据(真实用户)经常差好几倍。用 Google 官方的 web-vitals 库(GitHub,约 5 KB)或者手写 PerformanceObserver 都可以。手写版本:
// 上报到你的 RUM 后端
function reportLCP() {
let lcpEntry;
const observer = new PerformanceObserver((list) => {
// 每次 LCP 候选变化时保存最新值
lcpEntry = list.getEntries().at(-1);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
// 页面隐藏时上报(tab 切换 / 关闭 / bfcache 进入)
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && lcpEntry) {
observer.disconnect();
// 用 sendBeacon 保证在页面卸载时也能发出
navigator.sendBeacon('/rum', JSON.stringify({
metric: 'LCP',
value: lcpEntry.renderTime || lcpEntry.loadTime,
element: lcpEntry.element?.tagName + '.' + lcpEntry.element?.className,
url: location.pathname,
connection: navigator.connection?.effectiveType,
}));
}
}, { once: true });
}
reportLCP();
关键细节:LCP 必须在 visibilitychange 时上报,而不是 load,因为 LCP 可能在 load 之后还在变(比如慢速网络下的 hero 图晚到);用 sendBeacon 而不是 fetch,浏览器会保证在页面卸载时仍然发送。
后端把每次上报按 URL 聚合,算 p75、p95,就得到你自己的 CrUX 数据。这个数据比 Google CrUX 更实时(CrUX 是滚动 28 天),也能覆盖低流量页面。
7 个常见的 LCP 优化陷阱
- 给所有图片加 fetchpriority="high":等于什么都没加,浏览器只会尊重"相对"优先级。永远只标 1 张(最多 2 张)。
- LCP 图片用 loading="lazy":直接毁掉 LCP。lazy 只该给折叠线以下的图片用。
- preload 但用错 as 属性:
as="image" 写成 as="fetch" 会导致浏览器重复下载。
- preload 字体但没写 crossorigin:字体请求会被丢弃,preload 完全无效。
- 只优化 Lab 数据不看 Field:Lighthouse 分 100 但真实用户 LCP 是 5 秒的例子非常常见。
- 忽略 TTFB:TTFB 800ms 时无论怎么优化前端 LCP 都上不去。先看 Early Hints、边缘 SSR。
- 把非 LCP 元素当 LCP 优化:优化了半天发现 LCP 是页脚某段文字。永远先用 PerformanceObserver 定位。
常见问题
fetchpriority="high" 和 preload 到底选哪个?
如果 LCP 资源已经在 HTML 里(比如 <img>),直接用 fetchpriority="high"。如果 LCP 资源被 CSS 引用(background-image、字体),用 <link rel="preload" fetchpriority="high">。两者能组合使用,但不要给 HTML 里已有的 <img> 再加 preload,那是重复请求。
LCP 元素识别不对怎么办?
通常是 CSS 让某个大元素在视觉上不可见但仍占尺寸(比如 opacity: 0 的横幅)。用 PerformanceObserver 打印所有 LCP 候选,就能看到每一次候选切换。如果需要显式排除,可以给元素加 elementtiming 属性并在服务端过滤。
为什么加了 fetchpriority 之后 LCP 反而变差?
最常见原因是同时给太多资源标 high,稀释了 LCP 图片的优先级。另一种情况是把 fetchpriority="high" 加到了非 LCP 元素上,挤占了带宽。用 DevTools Network 面板的 Priority 列检查每个资源的实际优先级。
Early Hints 需要改后端代码吗?
不一定。Cloudflare、Fastly、Vercel Edge 都提供 "Smart Early Hints",CDN 会自动从上一次的 Link 响应头学习该 URL 的关键资源,下次请求自动发 103。零代码改动,很多站开开关就能拿到 10%–20% 的 LCP 收益。
SPA 里 LCP 怎么算?路由切换时会重新触发吗?
SPA 里 Core Web Vitals 的软导航支持在 2024 年进入 Chrome。开启 chrome://flags/#soft-navigation-heuristics 后,每次路由切换都会触发新的 LCP 观察。生产环境建议自己用 History API 拦截 pushState/replaceState,手动重置 LCP 观察窗口。web-vitals 库的 attribution 版本内置了这个逻辑。