搞了半天性能监控,FCP、LCP、TTI 到底哪个才是用户真正能感知到的“卡”?
我们多数人对“页面卡”的直觉其实是错的。
FCP 代表“最早看见”,LCP 代表“主要内容就位”,TTI 代表“终于能点”,但真正让用户觉得“卡死了”的,既不是 FCP 也不是 TTI,甚至不完全是 LCP——而是这三个指标之间那些巨大的时间差。
FCP:你看见的“白屏结束了”,但用户根本不领情
FCP(First Contentful Paint)测量的是浏览器首次渲染任何文字、图片、非空白 canvas 或 SVG 的时间点。它解决的是“我到底打开了这个页面没有”的焦虑。
问题是,用户对“卡”的感知阈值远比 FCP 复杂。2019 年 Google 在 Chrome User Experience Report 里分析过几百万个真实站点的数据,发现 FCP 从 1 秒恶化到 3 秒,用户跳出率只增加了 32%。而同样幅度的恶化发生在 LCP 上,跳出率飙升 106%。这个差距说明一件事:用户能忍受白屏久一点,但绝对不能忍受主要内容迟迟不出来。
我见过最典型的例子是某电商首页。他们的 FCP 稳定在 1.2 秒左右,因为顶部导航栏渲染得很快。但商品列表依赖一个 2MB 的 JSON 接口,LCP 经常跑到 8 秒以上。性能监控面板上 FCP 一片绿,用户投诉“页面打不开”的工单却堆成山。运维同学盯着 FCP 看了两周,完全搞错了方向。
所以 FCP 代表的是“安全感建立”的时刻,不是“能用”的时刻。用户看到导航栏出来了,知道页面没挂,接下来如果长时间没东西,他们会认为“这页面有问题”,而不是“网速慢”。
LCP:最接近用户直觉,但有个大坑
LCP(Largest Contentful Paint)测量的是视口内最大内容元素完成渲染的时间。一般来说,这个最大元素就是你想让用户看到的核心东西——首屏大图、文章标题、商品主图。
2019 年 Google 把 LCP 推上 CWV(Core Web Vitals)的核心位置不是拍脑袋决定的。他们内部做过大规模的眼动追踪实验,发现用户在页面加载过程中的注意力焦点,与 LCP 元素的重合度高达 78%。换句话说,LCP 卡了,用户的眼睛就在等那个位置。
但 LCP 有个致命的坑:它测量的元素会变。
我一个做新闻内容站的朋友踩过这个坑。他们首页的 LCP 元素一开始是一张 600KB 的封面图,LCP 稳定在 2.8 秒。后来产品经理要求在封面图上方加一个 GIF 动图 banner,LCP 元素立刻变成了那个 3MB 的 GIF,LCP 暴涨到 7 秒。更恶心的是,这个 GIF 在第三帧才显示完整内容,前两帧是纯色背景,浏览器在第二帧就判定 LCP 完成——但用户看到的是一块空白区域闪了一下,然后才出内容。
这就是 LCP 的“候选元素漂移”问题。浏览器在渲染过程中会不断重新评估哪个元素是“最大内容元素”,直到用户交互或页面完全加载为止。如果你的页面有延迟加载的大图、异步插入的 DOM 节点,LCP 元素可能在 2 秒、5 秒、甚至 8 秒时各换一次。最后报出来的 LCP 值是最后一个元素的时间,但用户感知到的“卡”是每次元素切换时的布局抖动。
我现在的习惯是,看 LCP 时一定要配合 Layout Shift 的数据一起看。如果 LCP 在 3 秒以上,且 CLS 超过 0.15,那基本可以断定页面在加载过程中发生过剧烈的视觉跳动。用户说的“页面闪来闪去”,就是这种感觉。
TTI:理论上最重要,实际上最难用
TTI(Time to Interactive)测量的是页面达到“完全可交互”状态的时间。具体定义是:从 FCP 开始,到页面在 5 秒内没有任何超过 50ms 的长任务,且没有超过两个正在进行的网络请求。
这个指标的设计逻辑很严谨,但落地非常痛苦。我跑过 300 多个页面的 Lighthouse 报告,发现 TTI 和用户的“感觉卡”之间经常对不上。
举个例子。一个后台管理系统的表格页,TTI 是 9.2 秒。但用户在第 4 秒就能点击分页器翻页了,因为表格组件在 3.8 秒时已经渲染完首屏数据,只是后台还在加载图表库和富文本编辑器。Lighthouse 判定 TTI 为 9.2 秒,是因为富文本编辑器初始化时产生了一连串 80-120ms 的长任务。但用户根本不关心编辑器什么时候初始化完,他们只关心表格能不能翻页。
这就是 TTI 的“实验室化”问题。它假设页面上的所有 JavaScript 都是同等重要的,但实际上用户只关心他当前要点的那个按钮能不能点。TTI 测出来的更像是“页面彻底安静了”的时刻,而不是“用户能干活了”的时刻。
更麻烦的是,TTI 在真实用户监控(RUM)里几乎没法准确采集。Long Task API 需要监听 PerformanceObserver,而且长任务的定义依赖主线程的繁忙程度,这和设备性能强相关。同一段代码,在 iPhone 15 Pro 上 TTI 是 2 秒,在红米 9A 上可能是 12 秒。你拿实验室数据做优化,上线后真实用户的 TTI 纹丝不动。
我现在的做法是,用 TBT(Total Blocking Time)替代 TTI 做实验室监控。TBT 测量的是 FCP 和 TTI 之间所有长任务的阻塞时间总和,这个值更能反映主线程的“拥堵程度”,而且和真实用户的交互延迟(INP)相关性更高。Google 在 2024 年 3 月正式把 INP 替换 FID 纳入 CWV,也侧面印证了这个方向。
什么指标才能真正捕捉到“卡”?
如果只留一个指标,我会选 INP(Interaction to Next Paint)。
INP 测量的是用户在整个页面生命周期中,所有点击、触摸、键盘输入事件的响应延迟的最大值(或接近最大值)。简单说,就是“你点了按钮,页面多久给你反应”。
这个指标残酷到什么程度呢?我在一个中台项目上跑过一个月的 INP 数据。用户点击“提交”按钮后,平均响应时间是 87ms,P75 是 120ms,看起来还不错。但 INP 报的是 680ms,因为有一次用户点击时,正好赶上浏览器在 GC,事件队列里积压了 600ms 才执行到他的点击回调。这个用户的体感就是“点了没反应,狂点好几下,然后突然跳了两页”。
这就是 INP 的价值。它不关心平均值,只关心最差的那几次。用户对“卡”的记忆是峰终定律驱动的——他只记得最卡的那一次。FCP、LCP、TTI 这些基于加载阶段的指标,完全捕捉不到这种“用着用着突然卡了”的情况。
部署 INP 监控也不复杂。用 web-vitals 库的 3.x 版本,一行代码就能拿到数据:
import {onINP} from 'web-vitals';
onINP((metric) => {
// metric.value 是延迟时间,单位毫秒
// metric.attribution.eventTarget 可以定位到具体哪个元素卡了
console.log('INP:', metric.value, 'ms', metric.attribution);
});
关键是 attribution 字段。它能告诉你卡顿发生在哪个 DOM 元素上、什么类型的事件、以及长任务的具体耗时分布。我靠这个定位过一个诡异的 Bug:用户在搜索框里打字,每输入三个字符就会卡一下。最后发现是输入框的 onChange 里调了一个未做防抖的接口,而接口在输入第三个字符时正好命中了一个慢 SQL。
那加载阶段的“卡”怎么办?
INP 只管交互延迟,不管加载体验。如果你的页面加载本身就慢,用户还没开始交互就觉得“这页面好慢”,INP 是看不出来的。
这时候 LCP 仍然是加载阶段最靠谱的指标,但需要配合两个细节一起看。
第一个是 LCP 子部分分解。Chrome DevTools 的 Performance 面板里,LCP 时间可以拆成四个阶段:TTFB(服务器响应时间)、资源加载延迟、资源加载耗时、元素渲染耗时。我看到太多人对着 LCP 数值发愁,却不看这四个阶段哪个是瓶颈。一个 LCP 5 秒的页面,如果 TTFB 就占了 3 秒,你优化前端代码到吐血也没用。
第二个是 LCP 的 P75 值,而不是平均值。Lighthouse 给你的是模拟环境下的单次数据,真实用户的 LCP 分布是一条长尾曲线。P50 可能在 2 秒,但 P75 在 5 秒,说明有 25% 的用户在经历严重的加载延迟。这些用户大概率是移动端弱网环境,或者设备性能差。只盯着平均值优化,你永远抓不到真正喊“卡”的那群人。
// 用 web-vitals 采集 LCP 并上报 P75
import {onLCP} from 'web-vitals';
const lcpValues = [];
onLCP((metric) => {
lcpValues.push(metric.value);
// 定期计算 P75 上报
if (lcpValues.length >= 100) {
const sorted = [...lcpValues].sort((a, b) => a - b);
const p75 = sorted[Math.floor(sorted.length * 0.75)];
navigator.sendBeacon('/analytics', JSON.stringify({lcpP75: p75}));
lcpValues.length = 0;
}
});
常见问题
FCP 很快但用户说白屏很久,怎么回事?
FCP 只要求“任何内容”渲染出来,不要求内容有意义。如果你的页面最先渲染的是一个透明 div 或者一个 1x1 的占位图,FCP 可能只有 0.8 秒,但用户看到的是白花花的屏幕。这种情况通常是因为关键 CSS 没内联,或者字体文件阻塞了文本渲染。去 DevTools 的 Performance 面板看 FCP 时刻的截图,如果截图上基本没内容,那 FCP 就是个假指标。
LCP 优化到 2 秒以内了,用户还是说加载慢?
检查两件事。第一,LCP 元素是不是用户真正关心的内容。如果 LCP 元素是一个巨大的 banner 图,但用户想看的商品列表在第二屏,那 LCP 再快也没用。第二,LCP 到用户可以交互之间还有多久。如果 LCP 是 1.8 秒,但 TBT 有 3 秒,意味着主要内容出来后,页面又卡了 3 秒才能点。用户的感觉是“图片出来了但页面死了”。
INP 和 FID 有什么区别,为什么 Google 要换?
FID(First Input Delay)只测量用户第一次交互的延迟,而且只记录输入延迟部分,不包含事件处理函数的执行时间。INP 测量整个页面生命周期的所有交互,并且记录从用户操作到下一帧绘制的完整时间。一个页面 FID 可能只有 20ms,但用户在第 10 次点击时遇到 500ms 的延迟,FID 完全看不见,INP 会精准捕捉到。这就是 Google 在 2024 年用 INP 替换 FID 的原因。
能不能只看 INP,不管 LCP 和 FCP?
不能。INP 只反映交互响应速度,如果你的页面 LCP 要 8 秒,用户等了 8 秒才看到内容,即使内容出来后交互很流畅,用户也已经走了。INP 和 LCP 是互补关系,不是替代关系。一个管加载体验,一个管运行时体验,两个都要看。