你是否好奇过,在不依赖第三方库的情况下,仅靠浏览器原生API,如何亲手搭建一个性能监控SDK?本文带你跳出“调包侠”的舒适区,通过拆解黑盒,深入探索底层真相,从零构建一个可控、可归因、可扩展的性能监控方案。
你将系统性地掌握以下核心能力:
- 「抓数据」: 透彻理解 FP、FCP、LCP、CLS、INP 等关键性能指标,并学会利用
PerformanceObserver API 从浏览器底层精准捕获它们。
- 「找原因」: 不仅知道“慢”,更要定位“为什么慢”。精准分析导致卡顿的 DOM 元素、资源文件或脚本片段。
- 「搞定 SPA」: 实现单页应用(SPA)路由切换时的分段性能统计,确保数据清晰、不打架。
预备知识:核心性能指标解读
面对一堆性能缩写无需畏惧。从前端用户体验出发,核心关注三件事:「快不快(Loading)」、「卡不卡(Interaction)」、「稳不稳(Visual Stability)」。
1. Loading (加载):关注关键渲染时刻
用户最怕面对白屏。此阶段我们关注三个关键瞬间:
| 指标 (全称) |
解释 |
典型场景 |
| 「FP」 (First Paint) 首次绘制 |
「屏幕亮了」 浏览器开始渲染任何视觉元素的时刻(哪怕只是背景色)。 |
屏幕从纯白变成浅灰色。 |
| 「FCP」 (First Contentful Paint) 首次内容绘制 |
「看到内容了」 浏览器渲染出第一个内容(文字、图片、Logo)的时刻。 |
页面上出现“Loading...”文字或导航栏 Logo。 |
| 「LCP」 (Largest Contentful Paint) 最大内容绘制 |
「主角登场」 视口内可见的最大图片或文本块渲染完成的时刻。这是 Google 最看重的加载指标之一。 |
电商详情页的主体商品大图加载完成。 |
「⏱️ 及格线」:FCP < 1.8s,LCP < 2.5s
2. Interaction (交互):保障操作流畅度
页面加载完成后,用户开始交互,此时最忌卡顿。
| 指标 (全称) |
解释 |
典型场景 |
| 「FID」 (First Input Delay) 首次输入延迟 |
「第一下没反应?」 用户第一次与页面交互(点击、输入)到浏览器开始处理该事件的时间差。 |
点击登录按钮后,过了一秒按钮才有反应。 |
| 「INP」 (Interaction to Next Paint) 下次绘制交互 |
「越用越卡?」 FID 的升级版。监控页面生命周期内所有交互的延迟,并取最慢的若干次进行评估。 |
在输入框中打字时,每个字符的显示都有明显的粘滞感。 |
| 「Long Task」 长任务 |
「谁在堵路?」 任何执行时间超过 50ms 的 JavaScript 任务,会阻塞主线程。 |
复杂计算脚本执行期间,页面点击无响应。 |
「⏱️ 及格线」:FID < 100ms,INP < 200ms,Long Task < 50ms
注:长任务在加载期也常见,会拉长白屏时间。因其最直接影响交互响应,故在此讨论。
3. Visual Stability (视觉稳定性):避免布局偏移
意外的布局移动是糟糕的用户体验。
| 指标 (全称) |
解释 |
典型场景 |
| 「CLS」 (Cumulative Layout Shift) 累积布局偏移 |
「手滑点错了!」 页面在加载过程中,元素发生的意外移动程度的累积分数。分数越低,页面越稳定。 |
刚想点击“取消”按钮,顶部突然插入的广告将页面内容挤下,导致误点“支付”。 |
「⏱️ 及格线」:CLS < 0.1
系统架构与功能设计
我们采用分层架构设计,确保SDK的轻量与可扩展性。系统由核心采集层、数据处理层、数据上报层和配置中心四大模块构成。
简而言之,职责明确:采集模块只管抓数据,处理模块负责清洗与归因,上报模块确保数据送达服务端,配置模块统一管理开关与参数。

架构要点:
- 「采集层」:分为 Loading、Interaction、VisualStability、Network 四大模块,各司其职。
- 「处理层」:进行数据清洗、格式化,并实现核心指标归因(如定位导致 LCP 慢的 DOM 元素)。
- 「上报层」:支持
sendBeacon 与 fetch keepalive 双保险机制,确保页面卸载时数据不丢失。
- 「配置中心」:管理环境区分、采样率、日志开关等。
1. 核心采集层 (Collectors) —— 按用户体验划分
这是SDK的心脏。我们按用户实际感受划分模块:
- Step 1: Loading (看得见吗?)
- 核心目标:监控白屏时间与关键内容渲染。
- 实现手段:利用
Paint Timing 与 Largest Contentful Paint API,捕获 FP、FCP、LCP。
- Step 2: Interaction (好用吗?)
- 核心目标:监控交互响应速度与流畅度。
- 实现手段:通过
Event Timing 监听交互延迟(FID/INP),并用 Long Task API 定位阻塞主线程的任务。
- Step 3: Visual Stability (稳不稳?)
- 核心目标:防止页面布局意外移动。
- 实现手段:结合
Layout Shift API 计算累积布局偏移(CLS)。
- Step 4: Network (为啥慢?)
- 核心目标:定位资源加载与接口请求的瓶颈。
- 实现手段:利用
Resource Timing API,深度解析资源与请求的 DNS、TCP、TTFB 等各阶段耗时。
2. 数据处理层 (Processor) —— 数据清洗与归因
- 清洗数据:将浏览器原生 API 提供的杂乱数据,转换成干净、统一的 JSON 格式。
- 增强归因:为数据添加上下文。例如,为 LCP 指标附加上对应慢元素的 CSS 选择器;尝试解析长任务的来源脚本。
3. 数据上报层 (Reporter) —— 确保数据可达
- 使命必达:优先使用
Navigator.sendBeacon() 确保页面卸载时数据能可靠发送;现代浏览器中也可使用 fetch(..., { keepalive: true }) 以支持自定义请求头。
- 省流优化:支持批量上报与实时上报结合,平衡数据完整性与网络开销。
4. 配置中心 (Configurator) —— 集中化管理
- 灵活可控:通过初始化
options 参数控制 SDK 行为。例如,开发环境开启详细日志,生产环境设置采样率,动态开关特定监控模块。
核心代码实现
项目结构
采用清晰的模块化目录结构,便于维护和扩展。
performance-monitor/
├── dist/ # 打包产物
├── src/ # 源码目录
│ ├── index.ts # 入口文件
│ ├── loading/ # 加载与绘制采集(FP/FCP/LCP/Load)
│ ├── interaction/ # 交互采集(FID/INP/LongTask)
│ ├── visualStability/ # 视觉稳定性(CLS)
│ ├── network/ # 资源与网络监控(ResourceTiming / API)
│ ├── report/ # 数据上报(sendBeacon / fetch keepalive)
│ └── util/ # 工具函数(DOM选择器生成/路由监听)
├── test/ # 测试用例与本地服务
│ ├── server.js
│ ├── index.html
│ └── case-*.js
├── package.json
├── rollup.config.js
└── tsconfig.json
设计思路:
src 目录按四大监控维度分工,职责单一。
- 使用 TypeScript 编写,直接调用
PerformanceObserver 等原生 API,保证数据来源的真实性与透明性。
- 提供完整的本地测试环境,开箱即用。
浏览项目的完整代码及示例可以访问 https://github.com/Teernage/performance-monitor 。
1. 主入口 (index.ts)
入口文件负责初始化并串联所有监控模块。
// src/index.ts
import { startFP, startFCP, startLCP, startLoad } from './loading';
import { startFID, startINP, startLongTask } from './interaction';
import { startCLS } from './visualStability';
import { startEntries, startRequest } from './network';
export default class PerformanceMonitor {
private options: any;
constructor(options: any = {}) {
this.options = { log: true, ...options };
}
init() {
// 1. 页面加载与渲染 (Loading & Rendering)
startFP();
startFCP();
startLCP();
startLoad();
// 2. 交互响应 (Interaction)
startFID();
startINP();
startLongTask();
// 3. 视觉稳定性 (Visual Stability)
startCLS();
// 4. 资源与网络 (Resource & Network)
startEntries();
startRequest();
if (this.options.log) {
console.log('Performance Monitor Initialized');
}
}
}
要点:
init() 方法一键启动所有监控。
- 通过
options 实现配置化管理。
- 模块间解耦,通过引入各模块的
start 函数串联。
2. Loading 监控实现
(1) FP & FCP 采集
利用 PerformanceObserver 监听 paint 类型条目。关键点:必须设置 buffered: true 以获取 SDK 初始化前已发生的绘制记录。
// src/loading/FP.ts
export function startFP() {
const entryHandler = (list: PerformanceObserverEntryList) => {
for (const entry of list.getEntries()) {
// 筛选 'first-paint'
if (entry.name === 'first-paint') {
observer.disconnect(); // FP 只发生一次,捕获后即可断开
const json = entry.toJSON();
console.log('FP Captured:', json);
// 构建上报数据结构
const reportData = {
...json,
type: 'performance',
name: entry.name,
pageUrl: window.location.href,
};
// sendReport(reportData);
}
}
};
const observer = new PerformanceObserver(entryHandler);
// buffered: true 是关键
observer.observe({ type: 'paint', buffered: true });
return () => observer.disconnect();
}
(2) LCP 采集
LCP 是动态的,需在页面隐藏或用户首次交互时上报最终值,并获取最大内容对应的 DOM 元素选择器。
// src/loading/LCP.ts
import { getSelector } from '../util/index';
export function startLCP() {
let lcpEntry: PerformanceEntry | undefined;
let hasReported = false;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 持续更新最新的 LCP 候选值
lcpEntry = entry;
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
const report = () => {
if (hasReported || !lcpEntry) return;
hasReported = true;
const json = (lcpEntry as any).toJSON();
const reportData = {
...json,
lcpTime: lcpEntry.startTime,
elementSelector: getSelector((lcpEntry as any).element), // 关键:元素归因
type: 'performance',
name: lcpEntry.name,
pageUrl: window.location.href,
};
console.log('LCP Final Report:', reportData);
};
// 页面隐藏或用户首次交互时,上报最终 LCP
const onHidden = () => {
if (document.visibilityState === 'hidden') report();
};
document.addEventListener('visibilitychange', onHidden, { once: true });
window.addEventListener('pagehide', report, { once: true });
// 监听用户首次交互
['click', 'keydown', 'pointerdown'].forEach((type) => {
window.addEventListener(type, report, { once: true, capture: true });
});
return () => {
observer.disconnect();
document.removeEventListener('visibilitychange', onHidden);
};
}
3. Interaction 监控实现
(1) FID & INP 采集
FID 仅捕获首次输入延迟,INP 则需持续监听所有交互事件。
// src/interaction/FID.ts
export function startFID() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceEventTiming[]) {
const delay = entry.processingStart - entry.startTime; // 核心计算
console.log('FID:', delay, entry.target);
observer.disconnect(); // FID 只看第一下
}
});
observer.observe({ type: 'first-input', buffered: true });
}
// src/interaction/INP.ts
export function startINP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 实际开发中需维护一个数组记录最慢的若干次交互
console.log('Interaction Latency:', entry.duration, entry.target);
}
});
// 注意:INP 监听的是 ‘event‘ 类型,并可设置阈值过滤短事件
observer.observe({ type: 'event', durationThreshold: 16, buffered: true });
}
(2) Long Task 采集
监控执行时间超过 50ms 的任务,并通过 attribution 字段初步判断任务来源(主页面或第三方 iframe)。
// src/interaction/longTask.ts
export function startLongTask() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
// attribution 包含任务来源信息,如 ‘self‘, ‘iframe‘ 等
console.log('LongTask:', entry.duration, (entry as any).attribution);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
}
4. Visual Stability (CLS) 监控实现
CLS 需要累积计算整个页面会话期间的布局偏移,并在页面卸载时上报总和。注意过滤用户交互导致的预期布局变化。
// src/visualStability/CLS.ts
export function startCLS() {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as LayoutShift[]) {
// 核心:剔除用户交互导致的预期偏移
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
const report = () => console.log('CLS Final:', clsValue);
// 双重保险监听页面卸载
window.addEventListener('pagehide', report, { once: true });
document.addEventListener(
'visibilitychange',
() => {
if (document.visibilityState === 'hidden') report();
},
{ once: true }
);
}
5. Network 监控实现
利用 Resource Timing API 监控所有资源加载,并可专门过滤出 fetch 和 xmlhttprequest 类型的请求进行详细分析。
// src/network/request.ts
export function startRequest() {
const entryHandler = (list: PerformanceObserverEntryList) => {
const data = list.getEntries();
for (const entry of data as PerformanceResourceTiming[]) {
// 过滤出 API 请求
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
const reportData = {
name: entry.name, // 请求URL
type: 'performance',
subType: entry.entryType,
sourceType: entry.initiatorType,
duration: entry.duration, // 总耗时
dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS耗时
tcp: entry.connectEnd - entry.connectStart, // TCP连接耗时
ttfb: entry.responseStart - entry.requestStart, // 首字节时间(服务端处理)
transferSize: entry.transferSize, // 传输大小
startTime: entry.startTime,
pageUrl: window.location.href,
};
console.log('Network Request:', reportData);
}
}
};
const observer = new PerformanceObserver(entryHandler);
observer.observe({ type: 'resource', buffered: true });
}
6. 工具函数:DOM 元素选择器生成
用于将导致性能问题的 DOM 元素转换为 CSS 选择器,便于精准定位。
// src/util/index.js
export function getElementSelector(element) {
if (!element || element.nodeType !== 1) return '';
// 如果有 id,直接返回 #id
if (element.id) {
return `#${element.id}`;
}
let path = [];
while (element) {
let name = element.localName;
if (!name) break;
if (element.id) {
path.unshift(`#${element.id}`);
break;
}
let className = element.getAttribute('class');
if (className) {
name += '.' + className.split(/\s+/).join('.');
}
path.unshift(name);
element = element.parentElement;
}
return path.join(' > ');
}
7. 数据上报层实现
确保在页面卸载时数据能可靠发送,采用 sendBeacon 优先,fetch + keepalive 降级的策略。
// src/report/index.ts
export const sendBehaviorData = (data: Record<string, any>, url: string) => {
const dataToSend = {
...data,
userAgent: navigator.userAgent,
};
// 1. 优先使用 sendBeacon
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(dataToSend)], {
type: 'application/json',
});
navigator.sendBeacon(url, blob);
return;
}
// 2. 降级方案:使用 fetch + keepalive
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataToSend),
keepalive: true, // 关键参数!保证页面关闭时请求能发出
}).catch((err) => {
console.error('上报失败:', err);
});
};
工程化构建与发布
1. 构建配置 (Rollup)
使用 Rollup 打包,生成 ESM、CJS、UMD 三种格式的产物,并输出 TypeScript 类型声明文件。
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/index.ts',
output: [
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true },
{ file: 'dist/index.esm.js', format: 'es', sourcemap: true },
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'PerformanceSDK',
sourcemap: true,
plugins: [terser()],
},
],
plugins: [
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
}),
],
};
2. 发布到 NPM
- 登录:在项目根目录执行
npm login。
- 构建:执行
npm run build 生成最新的 dist 目录。
- 发布:执行
npm publish --access public 发布公开包。
如何使用
NPM 安装 (推荐)
npm install performance-sdk
import PerformanceMonitor from 'performance-sdk';
const monitor = new PerformanceMonitor({
log: process.env.NODE_ENV === 'development', // 开发环境开启日志
sampleRate: 0.1, // 采样率10%
reportUrl: 'https://your-api.com/collect',
});
monitor.init();
CDN 引入
<script src="https://unpkg.com/performance-sdk@x.x.x/dist/index.umd.js"></script>
<script>
const monitor = new PerformanceSDK.PerformanceMonitor({ /* 配置 */ });
monitor.init();
</script>
总结与展望
至此,你已经完成了一个模块化、可归因、生产就绪的前端性能监控 SDK。它围绕四大支柱构建:
- Loading:确保页面快速呈现。
- Interaction:保障用户交互即时响应。
- Visual Stability:维持页面布局稳定。
- Network:洞察资源与接口性能瓶颈。
掌握这些浏览器原生API的调用和模块化采集思想,是深入前端性能优化领域的关键一步。这不仅是“造轮子”,更是将黑盒变为白盒,获得真正的技术掌控力。
下一步探索方向:
- 完善监控体系:结合错误监控(JS异常、资源加载失败、API请求异常)与行为监控(用户点击流、页面停留分析),构建全方位可观测性。
- 数据可视化:将上报的数据接入 Grafana、ELK Stack 等平台,搭建实时性能监控大盘。
- 智能告警:基于性能阈值(如 LCP > 4s,错误率飙升)配置钉钉、飞书等渠道的告警机器人。
性能优化是一场永无止境的旅程。希望这个从零开始的实践,能成为你构建更强大、更定制化监控系统的坚实基石。更多的前端工程化实践和性能优化案例,欢迎在云栈社区与广大开发者交流探讨。Happy Coding!