Performance Observer API 实战:前端性能监控的终极方案

3809 字
19 分钟
Performance Observer API 实战:前端性能监控的终极方案

“我们的页面性能怎么样?“——面对这个问题,很多团队的回答还停留在”感觉挺快的”或者”Lighthouse 跑了 90 分”的阶段。但真实用户的体验可能和你在办公室用 MacBook Pro + 千兆网络测出来的结果天差地别。

本文将深入讲解如何使用 Performance Observer API 构建一套完整的前端性能监控方案,覆盖 LCP、FID、CLS、INP 四大核心指标的采集原理与实现,并最终手写一个生产级的性能监控 SDK。

一、为什么需要真实用户监控(RUM)#

1.1 Lab Data vs Field Data#

  • Lab Data(实验室数据):Lighthouse、WebPageTest 等工具在受控环境下测出的数据
  • Field Data(现场数据):真实用户在各种设备、网络条件下的实际体验数据
你的 Lighthouse 分数:95
你在印度用户的 Android 手机上:35

这不是夸张。Google 的数据显示,不同设备间的性能差异可达 10 倍以上。只有采集真实用户数据,才能了解真实的性能状况。

1.2 Core Web Vitals#

Google 定义的三大核心指标(加上 INP 替代 FID):

指标全称衡量维度需改进
LCPLargest Contentful Paint加载速度≤2.5s≤4s>4s
INPInteraction to Next Paint交互响应≤200ms≤500ms>500ms
CLSCumulative Layout Shift视觉稳定性≤0.1≤0.25>0.25

二、PerformanceObserver 基础#

PerformanceObserver 是浏览器提供的性能数据订阅 API,你可以注册对特定类型性能事件的监听。

2.1 基本用法#

const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.entryType, entry.startTime, entry.duration);
}
});
// 监听资源加载
observer.observe({ type: 'resource', buffered: true });

buffered: true 是关键参数——它会返回在 Observer 创建之前已经发生的事件。如果不加这个参数,你可能会错过页面加载初期的性能数据。

2.2 支持的 entryType#

// 查看当前浏览器支持哪些类型
console.log(PerformanceObserver.supportedEntryTypes);
// 常见输出:
// ["element", "event", "first-input", "largest-contentful-paint",
// "layout-shift", "longtask", "mark", "measure", "navigation",
// "paint", "resource", "visibility-state"]

2.3 与 performance.getEntries() 的区别#

// 同步获取(可能遗漏正在进行的条目)
const entries = performance.getEntriesByType('resource');
// 异步订阅(推荐,不会遗漏)
const observer = new PerformanceObserver((list) => {
// 每当有新的 resource 条目产生时触发
});
observer.observe({ type: 'resource', buffered: true });

三、LCP(Largest Contentful Paint)采集#

LCP 衡量的是视口内最大可见内容元素完成渲染的时间。“最大内容”通常是:

  • <img> 元素
  • <video> 的封面图
  • 带有 background-image 的块级元素
  • 大段文本块

3.1 采集实现#

function observeLCP(callback) {
let lcpValue = 0;
let lcpEntry = null;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
// LCP 会持续更新,直到用户交互
// 最后一个条目就是最终的 LCP
const lastEntry = entries[entries.length - 1];
lcpValue = lastEntry.startTime;
lcpEntry = lastEntry;
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
// LCP 在用户首次交互后确定
// 使用 visibilitychange 和用户输入事件来确定最终值
const reportLCP = () => {
if (lcpEntry) {
observer.disconnect();
callback({
value: lcpValue,
element: lcpEntry.element?.tagName,
url: lcpEntry.url,
size: lcpEntry.size,
loadTime: lcpEntry.loadTime,
renderTime: lcpEntry.renderTime,
});
}
};
// 页面切到后台时上报
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
reportLCP();
}
});
// 用户首次交互时上报
['keydown', 'click', 'pointerdown'].forEach((type) => {
document.addEventListener(type, reportLCP, { once: true, capture: true });
});
return observer;
}
// 使用
observeLCP((data) => {
console.log('LCP:', data.value.toFixed(2), 'ms');
console.log('LCP 元素:', data.element);
console.log('资源 URL:', data.url);
});

3.2 LCP 的注意事项#

// LCP 可能被多次更新
// 例如:先渲染文字(LCP=500ms) → 再渲染大图(LCP=1200ms)
// 最终 LCP 取最后一个值
// LCP 不包括用户交互后渲染的内容
// 一旦用户点击/滚动/按键,LCP 就"冻结"了
// renderTime vs loadTime
// renderTime: 元素实际渲染到屏幕的时间
// loadTime: 资源加载完成的时间(对图片有意义)
// 跨域资源如果没有 Timing-Allow-Origin 头,renderTime 为 0

四、FID(First Input Delay)采集#

FID 衡量用户首次交互(点击、轻触、按键)到浏览器实际开始处理事件之间的延迟。注意:FID 已被 INP 取代,但仍值得了解。

function observeFID(callback) {
const observer = new PerformanceObserver((list) => {
const firstInput = list.getEntries()[0];
if (firstInput) {
const fid = firstInput.processingStart - firstInput.startTime;
callback({
value: fid,
eventType: firstInput.name,
startTime: firstInput.startTime,
processingStart: firstInput.processingStart,
processingEnd: firstInput.processingEnd,
duration: firstInput.duration,
});
observer.disconnect();
}
});
observer.observe({ type: 'first-input', buffered: true });
return observer;
}
// 使用
observeFID((data) => {
console.log(`FID: ${data.value.toFixed(2)}ms (${data.eventType})`);
});

FID 的局限性#

FID 只测量第一次交互的延迟,如果第一次交互很快但后续交互很慢,FID 无法反映。这就是 INP 取代 FID 的原因。

五、CLS(Cumulative Layout Shift)采集#

CLS 衡量页面整个生命周期中所有意外布局偏移的累计分数。

5.1 什么是布局偏移#

用户正在阅读文章 → 突然上方加载了一个广告 → 文章内容被推下去了

这种意外的视觉跳动就是布局偏移。注意,用户交互(如点击展开菜单)引起的布局变化不算CLS。

5.2 采集实现#

CLS 的计算方式在 2021 年后改为最大会话窗口算法:

function observeCLS(callback) {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 忽略用户交互引起的偏移
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// 如果与上一次偏移间隔 < 1秒,且整个会话 < 5秒,归入同一会话窗口
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
// 开始新的会话窗口
sessionValue = entry.value;
sessionEntries = [entry];
}
// CLS = 所有会话窗口中最大的那个
if (sessionValue > clsValue) {
clsValue = sessionValue;
}
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
// 页面隐藏时上报
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
observer.disconnect();
callback({
value: clsValue,
entries: sessionEntries.map((e) => ({
value: e.value,
startTime: e.startTime,
sources: e.sources?.map((s) => ({
node: s.node?.tagName,
previousRect: s.previousRect,
currentRect: s.currentRect,
})),
})),
});
}
});
return observer;
}
// 使用
observeCLS((data) => {
console.log('CLS:', data.value.toFixed(4));
data.entries.forEach((entry) => {
console.log('偏移源:', entry.sources);
});
});

5.3 常见 CLS 问题与修复#

<!-- 问题 1:图片没有尺寸 → 加载后撑开空间 -->
<!-- ❌ -->
<img src="photo.jpg">
<!-- ✅ 始终设置宽高 -->
<img src="photo.jpg" width="800" height="600">
<!-- 或使用 CSS aspect-ratio -->
<style>
.responsive-img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
</style>
<img class="responsive-img" src="photo.jpg">
/* 问题 2:Web 字体加载导致文字跳动 */
/* ✅ 使用 font-display: swap 或 optional */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
/* 更好:使用 size-adjust 匹配 fallback 字体的度量 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
size-adjust: 105%;
ascent-override: 95%;
descent-override: 22%;
line-gap-override: 0%;
}
/* 问题 3:动态插入的内容 */
/* ✅ 预留空间 */
.ad-slot {
min-height: 250px;
/* 使用 contain-intrinsic-size 更精确 */
contain-intrinsic-size: 300px 250px;
}

六、INP(Interaction to Next Paint)采集#

INP 衡量页面整个生命周期中所有交互的响应延迟,取第 98 百分位值(或近似值),全面反映页面的交互响应性。

6.1 INP 的组成#

INP = Input Delay + Processing Time + Presentation Delay
Input Delay: 事件进入队列到开始处理的时间(主线程可能在忙)
Processing Time: 事件处理函数的执行时间
Presentation Delay: 处理完到下一帧渲染上屏的时间

6.2 采集实现#

function observeINP(callback) {
// 收集所有交互的最大延迟
const interactionMap = new Map();
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 只关注有 interactionId 的事件(用户交互事件)
if (!entry.interactionId) continue;
const existing = interactionMap.get(entry.interactionId);
// 同一个交互可能触发多个事件(pointerdown + pointerup + click)
// 取 duration 最大的那个
if (!existing || entry.duration > existing.duration) {
interactionMap.set(entry.interactionId, {
id: entry.interactionId,
latency: entry.duration,
name: entry.name,
startTime: entry.startTime,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
inputDelay: entry.processingStart - entry.startTime,
processingTime: entry.processingEnd - entry.processingStart,
presentationDelay:
entry.startTime + entry.duration - entry.processingEnd,
target: entry.target?.tagName,
});
}
}
});
// 使用 durationThreshold 过滤掉极短的事件
observer.observe({
type: 'event',
buffered: true,
durationThreshold: 16, // 只记录 > 16ms 的事件
});
function getINP() {
const interactions = [...interactionMap.values()];
if (interactions.length === 0) return null;
// 按 latency 降序排列
interactions.sort((a, b) => b.latency - a.latency);
// 取第 98 百分位
// 简化算法:如果交互数 < 50,取最大值;否则忽略最差的 2%
const index = Math.min(
interactions.length - 1,
Math.floor(interactions.length * 0.02)
);
return interactions[index];
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
observer.disconnect();
const inp = getINP();
if (inp) {
callback({
value: inp.latency,
detail: inp,
totalInteractions: interactionMap.size,
});
}
}
});
return { observer, getINP };
}
// 使用
observeINP((data) => {
console.log('INP:', data.value, 'ms');
console.log('最慢交互类型:', data.detail.name);
console.log('Input Delay:', data.detail.inputDelay.toFixed(2), 'ms');
console.log('Processing Time:', data.detail.processingTime.toFixed(2), 'ms');
console.log('Presentation Delay:', data.detail.presentationDelay.toFixed(2), 'ms');
console.log('总交互数:', data.totalInteractions);
});

6.3 优化 INP 的策略#

// 1. 减少 Input Delay —— 避免长任务阻塞主线程
// ❌ 同步处理大量数据
button.addEventListener('click', () => {
processLargeDataset(data); // 阻塞 500ms
updateUI();
});
// ✅ 使用 scheduler.yield() 让出主线程
button.addEventListener('click', async () => {
// 先更新 UI 给用户反馈
showLoadingState();
// 让出主线程,让浏览器有机会渲染
await scheduler.yield();
// 再处理耗时操作
processLargeDataset(data);
await scheduler.yield();
updateUI();
});
// 2. 减少 Processing Time —— 拆分长任务
// ❌ 一个事件处理函数干了太多事
input.addEventListener('input', (e) => {
validateInput(e.target.value); // 10ms
filterResults(e.target.value); // 50ms
updateSuggestions(filtered); // 20ms
logAnalytics(e.target.value); // 5ms
// 总共 85ms
});
// ✅ 拆分为高优先级和低优先级
input.addEventListener('input', (e) => {
const value = e.target.value;
// 高优先级:用户立即需要看到的
validateInput(value);
// 低优先级:延迟处理
requestIdleCallback(() => {
filterResults(value);
updateSuggestions(filtered);
});
// 更低优先级:分析日志
queueMicrotask(() => logAnalytics(value));
});
// 3. 减少 Presentation Delay —— 减少渲染工作量
// 使用 contain: content 限制重排范围
// 使用虚拟列表减少 DOM 节点数
// 避免在事件处理中触发强制同步布局

七、长任务监控#

长任务(Long Task)是指耗时超过 50ms 的主线程任务,它是导致 FID/INP 恶化的直接原因。

function observeLongTasks(callback) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
callback({
duration: entry.duration,
startTime: entry.startTime,
// attribution 告诉你长任务来自哪里
attribution: entry.attribution?.map((attr) => ({
name: attr.name,
entryType: attr.entryType,
containerType: attr.containerType,
containerSrc: attr.containerSrc,
containerName: attr.containerName,
})),
});
}
});
observer.observe({ type: 'longtask', buffered: true });
return observer;
}
// 使用
observeLongTasks((task) => {
if (task.duration > 100) {
console.warn(
`⚠️ 检测到长任务: ${task.duration.toFixed(0)}ms`,
task.attribution
);
}
});

八、资源加载监控#

function observeResources(callback) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 过滤出关键信息
const resourceData = {
name: entry.name,
type: entry.initiatorType, // script, css, img, fetch, xmlhttprequest
duration: entry.duration,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
// 详细的时间分解
timing: {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
tls: entry.secureConnectionStart
? entry.connectEnd - entry.secureConnectionStart
: 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
},
// 缓存命中检测
cached: entry.transferSize === 0 && entry.decodedBodySize > 0,
};
callback(resourceData);
}
});
observer.observe({ type: 'resource', buffered: true });
return observer;
}
// 使用
observeResources((resource) => {
if (resource.duration > 1000) {
console.warn(`慢资源: ${resource.name} (${resource.duration.toFixed(0)}ms)`);
console.log(' DNS:', resource.timing.dns.toFixed(0), 'ms');
console.log(' TCP:', resource.timing.tcp.toFixed(0), 'ms');
console.log(' TTFB:', resource.timing.ttfb.toFixed(0), 'ms');
console.log(' 下载:', resource.timing.download.toFixed(0), 'ms');
}
});

九、自建性能监控 SDK#

把上面所有的采集能力整合成一个生产级的 SDK:

perf-monitor.js
class PerfMonitor {
constructor(config = {}) {
this.config = {
endpoint: config.endpoint || '/api/perf',
sampleRate: config.sampleRate || 1.0, // 采样率
appId: config.appId || 'default',
debug: config.debug || false,
...config,
};
this.metrics = {};
this.observers = [];
this.buffer = [];
this.flushTimer = null;
// 采样控制
this.shouldSample = Math.random() < this.config.sampleRate;
if (!this.shouldSample) return;
this._init();
}
_init() {
this._observeLCP();
this._observeCLS();
this._observeINP();
this._observeFID();
this._observeLongTasks();
this._observeNavigation();
// 页面卸载前发送数据
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this._flush();
}
});
// 定期发送缓冲数据
this.flushTimer = setInterval(() => this._flush(), 30000);
}
_observeLCP() {
try {
let lcpValue = 0;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
lcpValue = last.startTime;
this.metrics.lcp = {
value: lcpValue,
element: last.element?.tagName,
url: last.url,
size: last.size,
};
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
this.observers.push(observer);
} catch (e) {
this._debug('LCP not supported');
}
}
_observeCLS() {
try {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
const firstEntry = sessionEntries[0];
const lastEntry = sessionEntries[sessionEntries.length - 1];
if (
sessionValue &&
entry.startTime - lastEntry.startTime < 1000 &&
entry.startTime - firstEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
this.metrics.cls = { value: clsValue };
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
this.observers.push(observer);
} catch (e) {
this._debug('CLS not supported');
}
}
_observeINP() {
try {
const interactionMap = new Map();
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.interactionId) continue;
const existing = interactionMap.get(entry.interactionId);
if (!existing || entry.duration > existing.duration) {
interactionMap.set(entry.interactionId, entry);
}
}
// 计算当前 INP
const interactions = [...interactionMap.values()];
if (interactions.length > 0) {
interactions.sort((a, b) => b.duration - a.duration);
const idx = Math.min(
interactions.length - 1,
Math.floor(interactions.length * 0.02)
);
this.metrics.inp = {
value: interactions[idx].duration,
interactionCount: interactions.length,
};
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
this.observers.push(observer);
} catch (e) {
this._debug('INP not supported');
}
}
_observeFID() {
try {
const observer = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
if (entry) {
this.metrics.fid = {
value: entry.processingStart - entry.startTime,
eventType: entry.name,
};
observer.disconnect();
}
});
observer.observe({ type: 'first-input', buffered: true });
this.observers.push(observer);
} catch (e) {
this._debug('FID not supported');
}
}
_observeLongTasks() {
try {
let longTaskCount = 0;
let totalBlockingTime = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
longTaskCount++;
// TBT = 每个长任务超过 50ms 的部分之和
totalBlockingTime += Math.max(0, entry.duration - 50);
}
this.metrics.longTasks = {
count: longTaskCount,
tbt: totalBlockingTime,
};
});
observer.observe({ type: 'longtask', buffered: true });
this.observers.push(observer);
} catch (e) {
this._debug('LongTask not supported');
}
}
_observeNavigation() {
try {
const observer = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
if (entry) {
this.metrics.navigation = {
type: entry.type, // navigate, reload, back_forward, prerender
ttfb: entry.responseStart - entry.requestStart,
domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime,
load: entry.loadEventEnd - entry.startTime,
domInteractive: entry.domInteractive - entry.startTime,
// 详细时序
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
redirect: entry.redirectEnd - entry.redirectStart,
// 传输
transferSize: entry.transferSize,
};
}
});
observer.observe({ type: 'navigation', buffered: true });
this.observers.push(observer);
} catch (e) {
this._debug('Navigation timing not supported');
}
}
// 手动标记自定义指标
mark(name) {
performance.mark(name);
}
measure(name, startMark, endMark) {
try {
const measure = performance.measure(name, startMark, endMark);
this._addToBuffer({
type: 'custom',
name,
value: measure.duration,
timestamp: Date.now(),
});
} catch (e) {
this._debug('Measure failed:', e);
}
}
_addToBuffer(data) {
this.buffer.push({
...data,
appId: this.config.appId,
url: location.href,
userAgent: navigator.userAgent,
connection: navigator.connection
? {
effectiveType: navigator.connection.effectiveType,
rtt: navigator.connection.rtt,
downlink: navigator.connection.downlink,
}
: null,
deviceMemory: navigator.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency,
timestamp: Date.now(),
});
}
_flush() {
// 收集最终指标
this._addToBuffer({
type: 'web-vitals',
metrics: { ...this.metrics },
});
if (this.buffer.length === 0) return;
const payload = JSON.stringify(this.buffer);
this.buffer = [];
// 使用 sendBeacon 确保页面关闭时也能发送
if (navigator.sendBeacon) {
navigator.sendBeacon(this.config.endpoint, payload);
} else {
// 降级方案
fetch(this.config.endpoint, {
method: 'POST',
body: payload,
keepalive: true,
}).catch(() => {});
}
this._debug('Flushed metrics:', payload);
}
_debug(...args) {
if (this.config.debug) {
console.log('[PerfMonitor]', ...args);
}
}
destroy() {
this.observers.forEach((o) => o.disconnect());
clearInterval(this.flushTimer);
this._flush();
}
}
// 初始化
const monitor = new PerfMonitor({
endpoint: 'https://analytics.example.com/perf',
appId: 'my-app',
sampleRate: 0.1, // 10% 采样
debug: false,
});
// 自定义指标
monitor.mark('app-init-start');
// ... 初始化逻辑
monitor.mark('app-init-end');
monitor.measure('app-init', 'app-init-start', 'app-init-end');
export default PerfMonitor;

十、数据上报策略#

10.1 sendBeacon vs fetch#

// sendBeacon 的优势:
// 1. 页面卸载时也能发送(不会被浏览器取消)
// 2. 异步且不阻塞页面关闭
// 3. 有大小限制(通常 64KB)
// 降级策略
function sendData(url, data) {
const payload = JSON.stringify(data);
// 优先使用 sendBeacon
if (navigator.sendBeacon) {
const success = navigator.sendBeacon(url, payload);
if (success) return;
}
// 降级为 fetch + keepalive
fetch(url, {
method: 'POST',
body: payload,
keepalive: true,
headers: { 'Content-Type': 'application/json' },
}).catch(() => {
// 最终降级:同步 XMLHttpRequest(不推荐)
// 只在极端情况下使用
});
}

10.2 采样与数据量控制#

// 分级采样策略
function getSampleRate(metric) {
// 核心指标全量采集
if (['lcp', 'cls', 'inp'].includes(metric)) return 1.0;
// 资源加载 10% 采样
if (metric === 'resource') return 0.1;
// 长任务 50% 采样
if (metric === 'longtask') return 0.5;
// 其他 5% 采样
return 0.05;
}
// 数据压缩
function compressPayload(data) {
// 移除不必要的字段
const compressed = data.map((item) => ({
t: item.type,
v: Math.round(item.value),
ts: item.timestamp,
// ... 只保留必要字段
}));
return JSON.stringify(compressed);
}

十一、总结#

Performance Observer API 是前端性能监控的基石。通过本文,你应该掌握了:

  1. 四大 Web Vitals 指标的采集原理和实现:LCP、FID、CLS、INP
  2. 长任务和资源加载的监控方法
  3. 生产级 SDK 的完整设计:采样、缓冲、上报、容错
  4. 数据上报的最佳实践:sendBeacon、keepalive、采样控制

实际项目中,你可以直接使用 Google 的 web-vitals 库作为采集层——它经过了大量的边界情况处理和浏览器兼容性测试。但理解底层原理,才能在出问题时快速定位和解决。

性能优化不是一次性的工作,而是一个持续的过程。有了监控数据,你才能知道优化的效果,才能发现回退,才能对性能有真正的掌控力。

“You can’t improve what you can’t measure.” —— 这句话在前端性能领域尤其适用。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Performance Observer API 实战:前端性能监控的终极方案
https://boke.hackerdream.xyz/posts/performance-observer-api/
作者
晴天
发布于
2026-02-21
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录