可观测性篇 | 性能监控
前言
上一章我们聊了错误监控——知道页面”有没有出问题”。这一章我们来聊可观测性的另一个核心维度:性能监控——知道页面”快不快”。
作为前端开发者,我们常常花大量时间在”功能实现”上——页面渲染正确、交互逻辑正确、接口对接正确。但很多时候我们忽略了一个同样重要的问题:用户体验到底好不好?
你写的页面在你的 MacBook Pro 上跑得飞快,不代表在用户的千元安卓机上也一样流畅。你本地网络是 200Mbps 光纤,不代表用户在地铁里的 4G 信号也能秒开页面。
这就是性能监控的意义——用数据说话,而不是用感觉。
在面试中,性能监控是一个”区分度极高”的话题。能聊到 FCP、LCP 这些指标说明你知道”什么是好的性能”,能聊到 PerformanceObserver 说明你知道”怎么采集数据”,能聊到 sendBeacon 和采样策略说明你知道”怎么在生产环境落地”。这三个层次对应了初级、中级、高级前端的认知差异。
本章,我们就从性能指标是什么、怎么采集、怎么上报、怎么分析这四个维度,把前端性能监控的知识体系讲清楚。
诊断自测
在进入正文之前,先用几道题测测你目前对性能监控的理解。
Q1:FCP 和 LCP 分别衡量的是什么?为什么光看 FCP 不够,还需要 LCP?
点击查看答案
FCP(First Contentful Paint)衡量的是页面第一个有内容的像素渲染到屏幕上的时间,可能只是一个导航栏甚至一个 loading spinner。LCP(Largest Contentful Paint)衡量的是视口内最大内容元素(通常是主图或主标题)渲染完成的时间。FCP 只能告诉你”页面开始有东西了”,但用户真正关心的是”我想看的内容出来了没有”——这正是 LCP 的意义。一个页面 FCP 很快(loading 动画出现了)但 LCP 很慢(真正的内容迟迟不出现),用户体验依然很差。
Q2:performance.timing 和 PerformanceObserver 有什么区别?为什么现代监控方案更推荐后者?
点击查看答案
performance.timing 是旧版 Navigation Timing API,返回一个包含各时间点的对象,所有数据一次性获取。它的问题是:(1)只能获取页面导航阶段的数据,无法监测后续的动态内容;(2)时间点是绝对时间戳,计算不方便;(3)已被标记为 deprecated。PerformanceObserver 是新版 API,采用观察者模式,可以异步监听各类性能条目(paint、largest-contentful-paint、longtask 等),不会阻塞主线程,也能捕获页面加载后动态产生的性能数据。现代监控方案都基于 PerformanceObserver,因为它更灵活、更准确、对性能本身的影响更小。
Q3:为什么用 navigator.sendBeacon() 上报性能数据,而不是普通的 fetch 或 XMLHttpRequest?
点击查看答案
sendBeacon 专门为”页面卸载时发送数据”这个场景设计。它有几个关键优势:(1)请求在浏览器后台异步发送,不会阻塞页面卸载或下一个页面的加载;(2)即使页面已经关闭,浏览器也会保证把数据发出去,而 fetch 或 XHR 在 unload/beforeunload 中发送时可能被浏览器取消;(3)它是 POST 请求,可以携带数据。这让它成为上报”页面离开前最后一批数据”的最佳选择。
一、性能指标:用户体验的”度量衡”
要做性能监控,首先得知道”好性能”长什么样。Google 提出了一套以用户为中心的性能指标体系,也就是我们常说的 Web Vitals。
1.1 TTFB(Time to First Byte)
定义: 从用户发起导航请求,到浏览器收到响应的第一个字节所花的时间。
用户点击链接 ──> DNS 查询 ──> TCP 连接 ──> TLS 握手 ──> 服务器处理 ──> 第一个字节到达
|_________________________ TTFB __________________________|
TTFB 反映的是服务端响应速度 + 网络链路耗时。它不是直接的用户感知指标,但会影响后续所有指标——如果 TTFB 就很慢,FCP、LCP 必然也快不了。
参考值: 好的 TTFB 应该在 800ms 以内。
1.2 FCP(First Contentful Paint)
定义: 页面第一个文本、图片、SVG 或非白色 Canvas 渲染到屏幕上的时间。
FCP 回答的问题是:“页面是不是还是白屏?” 用户看到任何内容——哪怕只是一个 loading 文字——FCP 就算完成了。
参考值: 好的 FCP 应该在 1.8 秒以内。
1.3 LCP(Largest Contentful Paint)
定义: 视口内最大的内容元素完成渲染的时间。“最大内容元素”通常是 <img>、<video> 的海报帧、带背景图的块级元素、或大段文本块。
LCP 回答的问题是:“页面的主要内容加载好了吗?” 这是用户感知的”页面加载完成”的最关键指标。
参考值: 好的 LCP 应该在 2.5 秒以内。
需要注意的细节:
- LCP 会随着页面渲染不断更新——比如先渲染了标题(LCP = 标题),然后主图加载完成(LCP 更新为主图)
- 一旦用户产生交互(点击、滚动、键盘输入),LCP 就停止更新
- LCP 不考虑视口外的元素
1.4 FID / INP(交互响应性)
FID(First Input Delay): 用户第一次与页面交互(点击、触摸、按键)时,从事件触发到浏览器实际开始处理事件之间的延迟。
FID 衡量的是”页面看起来能点,但实际点了以后多久才有反应”。如果主线程正在执行一段长任务(比如解析大量 JS),用户的点击事件就得排队等着,这段等待时间就是 FID。
INP(Interaction to Next Paint): 从 2024 年 3 月起,Google 用 INP 替代了 FID 作为核心 Web Vital。INP 不只看”第一次”交互,而是综合衡量整个页面生命周期中所有交互的响应延迟,取其中较差的值作为最终得分。
为什么 INP 取代了 FID? 因为 FID 只看第一次交互,可能恰好那时主线程很闲,FID 很好看。但之后用户操作时主线程可能正在忙,体验很差。INP 更能反映真实的全程交互体验。
参考值: 好的 INP 应该在 200ms 以内。
1.5 CLS(Cumulative Layout Shift)
定义: 页面生命周期中所有意外布局偏移的累积得分。
什么是”意外布局偏移”?就是你正在读文章,突然上面插进来一张图,把你正在读的文字推下去了;或者你准备点一个按钮,突然上面加载完了一个广告,按钮位置变了,你点到了广告——这种体验极其糟糕。
参考值: 好的 CLS 应该在 0.1 以内。
常见导致 CLS 的原因:
- 图片/视频没有设置宽高(加载完成后撑开容器导致偏移)
- 动态插入 DOM 内容(广告、弹窗、toast)
- Web 字体加载导致的文字重排(FOIT/FOUT)
- 动态加载的组件没有预留空间
指标总结表
| 指标 | 衡量什么 | 好的阈值 | 用户感知 |
|---|---|---|---|
| TTFB | 服务端响应速度 | < 800ms | ”点了之后有没有反应” |
| FCP | 首次内容渲染 | < 1.8s | ”是不是白屏” |
| LCP | 最大内容渲染 | < 2.5s | ”主内容出来了没” |
| INP | 交互响应延迟 | < 200ms | ”点了之后卡不卡” |
| CLS | 布局稳定性 | < 0.1 | ”页面有没有乱跳” |
二、性能数据采集:Performance API 全解
知道了要关注哪些指标,下一步是”怎么在代码中采集这些数据”。
2.1 旧版 API:performance.timing(了解即可)
// ⚠️ 已废弃,但面试可能会问
const timing = performance.timing;
const dns = timing.domainLookupEnd - timing.domainLookupStart;
const tcp = timing.connectEnd - timing.connectStart;
const ttfb = timing.responseStart - timing.navigationStart;
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
const load = timing.loadEventEnd - timing.navigationStart;
这个 API 简单直接,但有明显的局限:
- 只能获取导航阶段的数据,无法监测后续的 LCP、CLS 等
- 返回的是绝对时间戳(Date.now() 精度),不够精确
- 同步获取,可能在数据还没 ready 的时候就去读了
- 已被标记为 deprecated,新版浏览器未来可能移除
现在的替代方案是 Navigation Timing Level 2,通过 performance.getEntriesByType('navigation') 获取,返回的是高精度的相对时间(DOMHighResTimeStamp)。
2.2 核心 API:PerformanceObserver
PerformanceObserver 是现代性能监控的基石。它基于观察者模式,可以异步监听浏览器产生的各类性能条目。
基本用法:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime, entry.duration);
}
});
// 监听 LCP
observer.observe({ type: 'largest-contentful-paint', buffered: true });
这里的 buffered: true 非常关键——它的意思是”把在我开始监听之前就已经产生的条目也给我”。如果不加这个参数,你在页面加载完才注册的 Observer 就会错过 FCP、LCP 等早期指标。
采集 FCP:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime);
}
}
}).observe({ type: 'paint', buffered: true });
采集 LCP:
new PerformanceObserver((list) => {
const entries = list.getEntries();
// LCP 会多次触发,取最后一个
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
采集 CLS:
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 只计算非用户输入触发的布局偏移
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
console.log('Current CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true });
注意 CLS 的计算有一个细节:hadRecentInput 为 true 表示这次偏移是由用户交互(如点击展开面板)引起的,属于”预期内的偏移”,不应该计入 CLS。
2.3 web-vitals 库:Google 的官方方案
手动用 PerformanceObserver 采集各个指标,需要处理很多边界情况(比如 LCP 在页面切到后台时应该停止、CLS 需要按”会话窗口”分组计算等)。Google 提供了一个官方的 web-vitals 库,帮你把这些细节都处理好了。
import { onCLS, onFID, onLCP, onFCP, onTTFB, onINP } from 'web-vitals';
onFCP(metric => {
console.log('FCP:', metric.value);
// metric.id - 唯一标识
// metric.name - 'FCP'
// metric.value - 具体数值(毫秒)
// metric.rating - 'good' | 'needs-improvement' | 'poor'
// metric.delta - 自上次报告以来的变化量
});
onLCP(metric => {
console.log('LCP:', metric.value, metric.rating);
});
onCLS(metric => {
console.log('CLS:', metric.value);
});
onINP(metric => {
console.log('INP:', metric.value);
});
onTTFB(metric => {
console.log('TTFB:', metric.value);
});
为什么推荐用 web-vitals?
- 边界情况处理完善:比如页面 visibility 变化、BFCache 恢复等
- 计算逻辑与 Chrome 一致:和 Lighthouse、CrUX 报告用的算法相同
- 体积极小:gzip 后约 1.5KB
- 提供
rating字段:直接告诉你指标是 good、needs-improvement 还是 poor
三、Long Task 检测
Long Task(长任务) 指的是占用主线程超过 50ms 的任务。在长任务执行期间,浏览器无法响应用户交互、无法进行渲染更新——用户会感觉到”卡顿”。
为什么是 50ms?
这和人类的感知阈值有关。研究表明,操作后 100ms 以内给出反馈,用户会觉得”即时响应”。而浏览器在接收到用户输入后,还需要一些时间做事件处理和渲染,留给 JS 执行的时间大约就是 50ms。超过这个时间,用户就可能感知到延迟。
检测方式
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long Task detected:', {
duration: entry.duration,
startTime: entry.startTime,
// 注意:出于安全原因,跨域脚本的详细信息会被隐藏
attribution: entry.attribution
});
// 可以上报给监控系统
if (entry.duration > 100) {
reportLongTask(entry);
}
}
}).observe({ type: 'longtask', buffered: true });
Long Task 与 INP 的关系
Long Task 是导致 INP 差的主要原因。当用户点击按钮时,如果主线程正在执行一个 200ms 的长任务,那么这次交互的响应延迟至少就是 200ms,INP 就会很差。
优化思路:
- 把长任务拆分成多个小任务(用
setTimeout(fn, 0)、requestIdleCallback、或scheduler.yield()) - 把计算密集型任务移到 Web Worker
- 减少不必要的 JS 执行(代码分割、Tree Shaking、延迟加载)
// 把一个长任务拆分为多个小任务
async function processLargeArray(items) {
const CHUNK_SIZE = 100;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
processChunk(chunk);
// 每处理完一批,让出主线程
await new Promise(resolve => setTimeout(resolve, 0));
}
}
四、性能数据上报策略
采集到性能数据后,需要把数据发送到后端进行分析。这一步看似简单,但在生产环境中有很多讲究。
4.1 上报方式对比
方式一:XMLHttpRequest / fetch
// ❌ 不推荐用于页面卸载场景
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics),
headers: { 'Content-Type': 'application/json' }
});
问题:在 unload 或 beforeunload 事件中发送的请求,浏览器可能会取消(因为页面已经在关闭了)。虽然可以用 keepalive: true 来缓解,但兼容性和行为不如 sendBeacon 稳定。
方式二:navigator.sendBeacon(推荐)
// ✅ 推荐
navigator.sendBeacon('/api/metrics', JSON.stringify(metrics));
sendBeacon 的优势前面已经讲过:后台异步发送、不阻塞页面卸载、浏览器保证发送完成。它是页面卸载时上报数据的最佳方案。
但 sendBeacon 也有局限:
- 只支持 POST 请求
- 无法获取响应内容
- 数据大小有限制(通常 64KB)
- 无法设置自定义请求头(如果要带 Token,需要用其他方式)
方式三:Image Beacon(传统方案)
// 兼容性最好,但只能 GET,数据量有限
const img = new Image();
img.src = `/api/metrics?data=${encodeURIComponent(JSON.stringify(metrics))}`;
通过创建一个 1x1 的透明图片来发送请求,优点是兼容性极好(甚至兼容 IE6)、不受跨域限制。缺点是只能用 GET、URL 长度有限制。
4.2 上报时机
不要在指标产生的瞬间立即上报。 频繁的网络请求本身就会影响性能,而且页面生命周期中的指标会不断更新(比如 LCP 和 CLS)。
推荐的策略是:
class MetricsCollector {
constructor() {
this.buffer = [];
this.setupFlush();
}
add(metric) {
this.buffer.push({
name: metric.name,
value: metric.value,
rating: metric.rating,
timestamp: Date.now(),
url: location.href,
// 附加上下文信息
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory
});
}
setupFlush() {
// 策略 1:页面卸载时上报
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
// 策略 2:定时批量上报(兜底)
setInterval(() => {
if (this.buffer.length > 0) {
this.flush();
}
}, 30000);
// 策略 3:缓冲区满时上报
// 在 add() 方法中检查 buffer.length
}
flush() {
if (this.buffer.length === 0) return;
const data = JSON.stringify(this.buffer);
this.buffer = [];
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/metrics', data);
} else {
// fallback
fetch('/api/metrics', {
method: 'POST',
body: data,
keepalive: true
});
}
}
}
为什么监听 visibilitychange 而不是 unload?因为在移动端,用户切走 App 后浏览器可能直接杀掉进程,unload 事件根本不会触发。而 visibilitychange 在页面从”可见”变为”隐藏”时一定会触发,覆盖面更广。
4.3 采样
在流量大的网站上,如果每个用户的每次访问都上报,数据量会非常恐怖。合理的做法是采样上报:
// 10% 的用户上报详细性能数据
const SAMPLE_RATE = 0.1;
const shouldSample = Math.random() < SAMPLE_RATE;
if (shouldSample) {
initPerformanceMonitoring();
}
采样需要注意:
- 采样决策应该在页面加载初期做出,并且在整个页面生命周期中保持一致
- 采样率根据流量调整:日活百万级用 1%~5%,日活万级用 50%~100%
- 对于性能特别差的用户(比如 LCP > 10s),可以提高采样率,重点关注
4.4 空闲上报
对于不那么紧急的性能数据,可以利用浏览器的空闲时段来上报,避免影响用户的交互体验:
function reportWhenIdle(data) {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
navigator.sendBeacon('/api/metrics', JSON.stringify(data));
}, { timeout: 5000 }); // 最多等 5 秒
} else {
// fallback:延迟上报
setTimeout(() => {
navigator.sendBeacon('/api/metrics', JSON.stringify(data));
}, 3000);
}
}
五、性能分析工具
有了数据采集和上报,还需要工具来帮助分析和定位问题。
5.1 Lighthouse
Lighthouse 是 Chrome 内置的网页质量审计工具,可以通过 Chrome DevTools 的 Lighthouse 面板、命令行、或 Node.js API 运行。
它的工作原理:
- 打开一个 Chrome 实例(可以无头模式)
- 模拟特定的网络和 CPU 条件(默认模拟中等移动设备)
- 加载目标页面
- 采集性能指标
- 根据一套评分算法给出 0-100 的分数和优化建议
关键注意事项:
- Lighthouse 在模拟环境中运行,分数和真实用户体验可能有差异
- 每次运行结果会有波动(网络、CPU 负载等因素),建议跑多次取中位数
- Lighthouse 的性能评分权重会随版本更新调整,不要把分数当作绝对指标
- 它给出的优化建议非常有价值,比如”推迟加载屏幕外图片”、“移除未使用的 CSS”等
5.2 Chrome DevTools Performance 面板
Performance 面板是做精细性能调优的利器。它能录制一段时间内浏览器的所有活动,以火焰图(Flame Chart)的形式展示。
录制后能看到什么:
- Main 线程活动:JS 执行、样式计算、布局、绘制
- Long Tasks:超过 50ms 的任务会被红色标记
- 帧率:每一帧的耗时,帮你发现掉帧
- 网络请求:资源加载的时序和耗时
- Web Vitals 标记:FP、FCP、LCP、Layout Shift 等关键时间点
常见分析流程:
- 打开 DevTools → Performance 面板
- 点击录制按钮(或 Ctrl+E)
- 执行你要分析的操作(比如页面加载、滚动、点击)
- 停止录制
- 在火焰图中找到最耗时的函数调用
- 结合 Summary、Bottom-Up、Call Tree 面板分析调用栈
一个实用技巧: 在分析 LCP 问题时,可以在 Performance 面板中勾选 “Screenshots”,录制后直接看到每一帧的渲染截图,直观地看到”什么时候内容才出来”。
5.3 实验室数据 vs 现场数据
这是一个很重要的概念区分:
- 实验室数据(Lab Data):在受控环境中采集的数据,比如 Lighthouse 跑出来的分数。优点是可复现、方便调试;缺点是不代表真实用户体验。
- 现场数据(Field Data / RUM Data):在真实用户的设备和网络上采集的数据,也叫 RUM(Real User Monitoring)。优点是反映真实体验;缺点是受设备、网络、地理位置等因素影响,波动大。
面试中如果被问到”你们怎么做性能监控”,最好的回答是两者结合: Lighthouse 用于开发阶段的性能基线检测和 CI 集成,RUM 用于线上真实用户体验的持续监控。
常见误区
误区一:“Lighthouse 跑到 90 分就说明性能好”
Lighthouse 分数是在模拟环境下测出来的,可能和真实用户体验差距很大。真正有说服力的是现场数据(Field Data)——来自真实用户设备和网络的 Web Vitals 数据。而且 Lighthouse 的模拟设备是中等移动设备 + 4G 网络,你的目标用户群体可能完全不同。永远不要只看 Lab Data 就下结论。
误区二:“把所有指标一股脑全报上去”
过度上报不仅浪费带宽和服务器资源,还可能反过来影响页面性能(讽刺吧?你的性能监控代码拖慢了性能)。应该有选择地采集关键指标、合理采样、批量上报。监控系统本身的开销应该尽可能低。
误区三:“FCP 够快就行了”
FCP 只表示”屏幕上出现了第一个内容”,可能只是一个 loading spinner 或骨架屏。用户真正关心的是有意义的主内容何时可见——这是 LCP 的职责。此外,即使 LCP 也不错,如果页面在加载过程中不断抖动(CLS 高)、交互卡顿(INP 高),用户体验依然很差。性能是多维度的,不能只盯一个指标。
误区四:“performance.timing 就够用了”
performance.timing 是 Navigation Timing Level 1 的产物,已经被标记为 deprecated。它只能获取页面导航阶段的时间点,无法采集 LCP、CLS、INP 等现代指标。更重要的是,它是同步读取、一次性返回的,在 SPA 场景下基本没用。现代性能监控应该基于 PerformanceObserver,或者直接用 web-vitals 库。
小结
本章我们从性能指标体系出发,完整梳理了前端性能监控的核心知识。
核心要点
- Web Vitals 五大指标:TTFB(服务端响应)、FCP(首次内容渲染)、LCP(最大内容渲染)、INP(交互响应)、CLS(布局稳定性)——它们从不同维度度量用户的真实体验
- PerformanceObserver 是现代性能采集的基石:异步、非阻塞、支持各类性能条目,配合
buffered: true不漏数据 - web-vitals 库:Google 官方封装,处理了大量边界情况,生产环境推荐直接使用
- Long Task > 50ms 会导致用户感知到卡顿,是 INP 差的主要原因
- 上报策略:sendBeacon 发送、visibilitychange 触发、批量缓冲、合理采样
- Lab Data + Field Data 结合:Lighthouse 用于开发阶段,RUM 用于线上监控
本章思维导图
- 性能指标(Web Vitals)
- TTFB:服务端响应 + 网络耗时(< 800ms)
- FCP:首次内容渲染(< 1.8s)
- LCP:最大内容渲染(< 2.5s)
- INP:全程交互响应延迟(< 200ms,替代 FID)
- CLS:累积布局偏移(< 0.1)
- 数据采集
- performance.timing(已废弃,了解即可)
- PerformanceObserver(现代方案,异步 + buffered)
- web-vitals 库(Google 官方,推荐生产使用)
- Long Task 检测
- 定义:主线程 > 50ms 的任务
- 与 INP 的关系
- 优化:任务拆分、Web Worker、代码分割
- 数据上报
- sendBeacon(推荐,页面卸载安全)
- visibilitychange 触发上报
- 批量缓冲 + 采样
- 空闲上报(requestIdleCallback)
- 分析工具
- Lighthouse:模拟环境审计,CI 集成
- DevTools Performance:火焰图精细分析
- Lab Data vs Field Data(RUM)
练习挑战
第一题 ⭐:指标识别
请说出以下用户体验问题分别对应哪个 Web Vital 指标:
- 用户打开页面后,白屏了 3 秒才出现内容
- 用户正在阅读文章,突然页面跳了一下,因为上方的广告加载完了
- 用户点击了”提交”按钮,但过了 500ms 页面才有反应
点击查看答案
- FCP(如果 3 秒后首次出现任何内容)和 LCP(如果 3 秒后才出现主要内容)。具体要看”出现的内容”是什么——如果是 loading spinner,FCP 可能在 1 秒就完成了,但 LCP 要到 3 秒。
- CLS(Cumulative Layout Shift)。广告加载完后撑开容器,导致下方内容被推移,产生意外的布局偏移。
- INP(Interaction to Next Paint)。用户交互后 500ms 才有视觉反馈,远超 200ms 的阈值。这通常是因为主线程有长任务阻塞了事件处理。
第二题 ⭐⭐:实现一个简易性能采集器
请基于 PerformanceObserver,写一个采集 FCP 和 LCP 的函数,并在 visibilitychange 时通过 sendBeacon 上报。
点击查看答案
function initPerfMonitor(reportUrl) {
const metrics = {};
// 采集 FCP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
metrics.fcp = entry.startTime;
}
}
}).observe({ type: 'paint', buffered: true });
// 采集 LCP(取最后一个)
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
metrics.lcp = lastEntry.startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });
// 页面隐藏时上报
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && Object.keys(metrics).length > 0) {
const payload = {
...metrics,
url: location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent
};
navigator.sendBeacon(reportUrl, JSON.stringify(payload));
}
});
}
initPerfMonitor('/api/perf-metrics');
要点:
- FCP 通过监听
paint类型获取,过滤name === 'first-contentful-paint' - LCP 会多次触发更新,始终取最后一个值
- 使用
visibilitychange而非unload,因为移动端unload可能不触发 buffered: true确保不遗漏已产生的条目
第三题 ⭐⭐⭐:Long Task 与用户交互的关联分析
假设你在生产环境中发现 INP 指标很差(P75 = 450ms),你会如何结合 Long Task 检测来定位问题?请设计一个方案,要求:
- 检测所有 Long Task
- 当用户发生交互时,判断当前是否有 Long Task 正在执行
- 把这个关联信息上报,帮助后端分析”哪些长任务导致了交互延迟”
点击查看答案
function initINPDiagnostics(reportUrl) {
let activeLongTasks = [];
// 1. 收集 Long Task 信息
new PerformanceObserver((list) => {
const now = performance.now();
for (const entry of list.getEntries()) {
activeLongTasks.push({
start: entry.startTime,
end: entry.startTime + entry.duration,
duration: entry.duration
});
}
// 只保留最近 5 秒内的 Long Task(避免内存泄漏)
activeLongTasks = activeLongTasks.filter(t => now - t.end < 5000);
}).observe({ type: 'longtask', buffered: true });
// 2. 监听用户交互,查看交互发生时是否有 Long Task
const interactionEvents = ['click', 'keydown', 'pointerdown'];
const reports = [];
interactionEvents.forEach(eventType => {
document.addEventListener(eventType, (e) => {
const interactionTime = performance.now();
// 找出与当前交互时间重叠的 Long Task
const blockingTasks = activeLongTasks.filter(task =>
task.start <= interactionTime && task.end >= interactionTime
);
if (blockingTasks.length > 0) {
reports.push({
eventType,
interactionTime,
targetTag: e.target?.tagName,
targetId: e.target?.id,
blockingTasks: blockingTasks.map(t => ({
duration: Math.round(t.duration),
waitTime: Math.round(t.end - interactionTime)
})),
timestamp: Date.now()
});
}
}, { capture: true, passive: true });
});
// 3. 定时 + 页面隐藏时上报
const flush = () => {
if (reports.length === 0) return;
const data = JSON.stringify(reports.splice(0));
navigator.sendBeacon(reportUrl, data);
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
setInterval(flush, 30000);
}
核心思路:
- Long Task 的
startTime + duration确定了任务的时间区间 - 当用户交互事件触发时,通过
performance.now()获取当前时间 - 如果当前时间落在某个 Long Task 的区间内,说明该长任务阻塞了这次交互
waitTime(长任务结束时间 - 交互时间)就是用户需要等待的最短时间- 后端拿到这些数据后,可以分析”哪些页面、哪些操作最容易被长任务阻塞”
- 结合 Source Map 和调用栈信息,可以进一步定位到具体的代码
自我检测
读完本章后,对照下面的清单检验自己的掌握程度:
- 能列出 Web Vitals 五大核心指标(TTFB、FCP、LCP、INP、CLS),并说清楚每个指标衡量的是什么用户体验维度
- 能解释 FID 和 INP 的区别,以及为什么 Google 用 INP 替代了 FID
- 能用 PerformanceObserver 采集 FCP、LCP、CLS 数据,并理解
buffered: true的作用 - 能说出 web-vitals 库相比手动采集的优势
- 能解释 Long Task 的定义(> 50ms)和它对用户体验的影响
- 能说出 sendBeacon 相比 fetch/XHR 在数据上报场景的优势
- 能解释为什么监听
visibilitychange比unload更可靠 - 能区分 Lab Data 和 Field Data,并说出各自的适用场景
- 能设计一个合理的性能数据上报策略(批量、采样、空闲上报)
- 能使用 Chrome DevTools Performance 面板录制并分析一个页面的性能瓶颈
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90