前言
做前端开发,性能优化是绕不过去的话题。线上用户反馈页面卡顿,老板说首屏加载太慢,产品经理要求 LCP 控制在 2.5 秒以内------这些需求几乎每个前端工程师都会遇到。
但很多人在面对性能问题时,要么凭感觉猜测瓶颈,要么只会跑一下 Lighthouse 看看分数,缺乏一套系统的排查和监控流程。实际上,Chrome DevTools、Lighthouse、Performance API 这三个工具各有分工,组合使用才能形成「发现问题 - 量化问题 - 持续监控」的完整闭环。
本文会从实操角度出发,手把手讲解这三个工具的核心用法,并在最后给出一个将三者串联起来的实战工作流。文中所有代码示例均可直接运行,适合有一定基础的前端开发者参考。
lua
+------------------+ +------------------+ +------------------+
| Chrome DevTools | | Lighthouse | | Performance API |
| (定位问题) | ---> | (量化评分) | ---> | (生产监控) |
| | | | | |
| - Performance | | - 跑分 + 建议 | | - 自定义指标 |
| - Network | | - CI 集成 | | - 上报告警 |
+------------------+ +------------------+ +------------------+
| | |
+------------- 性能优化闭环 ---------------------------+
一、Chrome DevTools Performance 面板
1.1 如何录制性能剖析
打开 Chrome DevTools(F12),切换到 Performance 面板。在录制前,建议做以下准备工作:
- 开启「无痕模式」,避免浏览器插件干扰
- 在 Performance 面板勾选「Screenshots」和「Web Vitals」
- 将 CPU 节流设置为 4x slowdown,模拟中端设备
- 将 Network 设置为 Fast 3G,模拟弱网环境
点击左上角的圆形录制按钮(或使用 Ctrl+E),然后在页面上执行你要分析的操作(比如页面加载、滚动、点击按钮等),操作完成后再次点击停止录制。
ini
录制流程:
[开启无痕模式] --> [打开 DevTools] --> [设置 CPU/网络节流]
|
v
[点击录制] --> [执行页面操作] --> [停止录制]
|
v
[分析火焰图 / 长任务 / 帧率]
1.2 火焰图(Flame Chart)怎么看
录制完成后,你会看到一张色彩丰富的火焰图。理解火焰图是性能调优的基本功。
横轴:时间线,从左到右表示时间推移。
纵轴:调用栈深度,从上到下表示函数调用关系,上层函数调用下层函数。
颜色含义:
| 颜色 | 含义 | 常见内容 |
|---|---|---|
| 黄色 | JavaScript 执行 | 函数调用、事件处理、定时器回调 |
| 紫色 | 样式计算与布局(Layout) | Recalculate Style、Layout |
| 绿色 | 绘制(Paint) | Paint、Composite Layers |
| 灰色 | 系统级任务 | Task、Run Microtasks |
| 红色三角 | 长任务标记 | 超过 50ms 的任务会被标记 |
一个典型的页面加载火焰图结构大致如下:
lua
时间轴 >>>
Main Thread:
|-- Parse HTML --|
|-- Evaluate Script (bundle.js) --|
|-- Run Microtasks --|
|-- Recalculate Style --|
|-- Layout --|
|-- Paint --|
长任务(> 50ms)会在顶部用红色条纹标注
1.3 识别长任务(Long Tasks)
长任务是指主线程上执行时间超过 50ms 的任务。长任务会阻塞用户交互,导致页面卡顿。
在 Performance 面板中,长任务会在时间轴顶部用红色条纹标记。点击某个长任务,底部的 Summary 面板会显示该任务的时间分布:
yaml
Summary 面板示例:
Total Time: 128.3 ms
Self Time: 12.1 ms
Breakdown:
Scripting: 89.2 ms (69.5%) <-- JavaScript 执行占比最高
Rendering: 24.7 ms (19.3%)
Painting: 8.1 ms (6.3%)
System: 6.3 ms (4.9%)
实战技巧:如果 Scripting 占比最高,展开火焰图找到耗时最长的函数;如果 Rendering 占比高,很可能是触发了强制回流。
1.4 识别强制回流(Forced Reflow)
强制回流是性能杀手。当 JavaScript 在修改 DOM 样式后立即读取布局属性时,浏览器被迫同步执行布局计算。
在 Performance 面板中,强制回流会在火焰图中显示为紫色的「Layout」块,并带有红色的警告三角标记「Forced reflow is a likely performance bottleneck」。
以下是一个典型的强制回流代码及其修复:
typescript
// 错误示范:循环中反复触发强制回流
function badLayout(elements: HTMLElement[]): void {
for (const el of elements) {
// 写操作
el.style.width = '100px';
// 紧跟读操作 -> 触发强制回流!
const height = el.offsetHeight;
el.style.height = `${height * 2}px`;
}
}
// 正确做法:先批量读,再批量写(读写分离)
function goodLayout(elements: HTMLElement[]): void {
// 阶段 1:批量读取
const heights = elements.map((el) => el.offsetHeight);
// 阶段 2:批量写入(不再触发强制回流)
elements.forEach((el, i) => {
el.style.width = '100px';
el.style.height = `${heights[i] * 2}px`;
});
}
也可以使用 requestAnimationFrame 将写操作推迟到下一帧:
typescript
function safeLayout(el: HTMLElement): void {
// 先读
const currentHeight = el.offsetHeight;
// 写操作推迟到下一帧
requestAnimationFrame(() => {
el.style.height = `${currentHeight + 10}px`;
});
}
常见触发强制回流的属性:
| 读取属性 | 说明 |
|---|---|
| offsetTop/Left/Width/Height | 元素的布局尺寸和位置 |
| clientTop/Left/Width/Height | 元素的可视区域尺寸 |
| scrollTop/Left/Width/Height | 元素的滚动位置和尺寸 |
| getComputedStyle() | 获取计算后的样式 |
| getBoundingClientRect() | 获取元素的边界矩形 |
二、Chrome DevTools Network 面板
2.1 瀑布流分析
Network 面板的瀑布流(Waterfall)是分析网络性能的利器。每个请求条的颜色和长度都有含义:
lua
瀑布流各阶段:
|-- DNS Lookup --|-- Initial Connection --|-- SSL --|-- TTFB --|-- Content Download --|
(深绿色) (橙色) (紫色) (绿色) (蓝色)
各阶段含义与优化方向:
| 阶段 | 含义 | 耗时长的优化方向 |
|---|---|---|
| Queueing | 请求排队等待 | 减少并发请求数、升级 HTTP/2 |
| DNS Lookup | DNS 解析 | 使用 dns-prefetch、减少域名数 |
| Initial Connection | TCP 连接建立 | 使用 preconnect、启用 keep-alive |
| SSL | TLS 握手 | 升级 TLS 1.3、启用 Session Resumption |
| TTFB | 首字节等待时间 | 优化服务端响应、使用 CDN |
| Content Download | 内容下载 | 压缩资源、使用 Brotli/Gzip |
2.2 瓶颈识别实战
打开 Network 面板后,按照以下步骤排查:
步骤一:按大小排序,找到体积最大的资源
点击 Size 列头进行排序,重点关注超过 200KB 的资源。常见问题包括:
- 未压缩的 JavaScript 包
- 未优化的图片
- 冗余的第三方库
步骤二:按时间排序,找到耗时最长的请求
点击 Time 列头排序,关注 TTFB 过长的请求(通常超过 600ms 就需要关注)。
步骤三:使用筛选功能聚焦特定资源类型
lua
常用过滤器:
larger-than:100k -- 大于 100KB 的资源
-domain:cdn.example.com -- 排除 CDN 域名
status-code:304 -- 查看命中缓存的请求
is:from-cache -- 查看所有从缓存加载的请求
mime-type:image -- 只看图片资源
步骤四:检查请求优先级
在 Waterfall 列右键选择「Priority」列,查看浏览器为每个请求分配的优先级。如果关键资源(如首屏 CSS、主 JS 包)优先级偏低,可以使用 fetchpriority 属性提升:
html
<!-- 提升关键图片的加载优先级 -->
<img src="hero.webp" fetchpriority="high" alt="Hero Image" />
<!-- 降低非关键脚本的加载优先级 -->
<link rel="preload" href="analytics.js" as="script" fetchpriority="low" />
2.3 利用 Coverage 面板检查代码覆盖率
按 Ctrl+Shift+P 打开命令面板,搜索「Show Coverage」。点击录制后刷新页面,Coverage 面板会显示每个 JS/CSS 文件的使用比例。
erlang
Coverage 面板示例输出:
URL Type Total Bytes Unused Bytes Usage
vendor.chunk.js JS 342 KB 198 KB 42.1%
main.chunk.js JS 156 KB 67 KB 57.1%
styles.css CSS 89 KB 52 KB 41.6%
红色条纹 = 未使用的代码 绿色条纹 = 已执行的代码
如果某个文件的未使用比例超过 50%,说明存在大量冗余代码,应该考虑代码分割或 tree-shaking。
三、Lighthouse
3.1 如何运行 Lighthouse
Lighthouse 有三种运行方式:
方式一:DevTools 内置面板
打开 DevTools -> Lighthouse 面板 -> 选择分析类别(Performance、Accessibility 等)-> 点击「Analyze page load」。
方式二:命令行运行(推荐用于 CI)
bash
# 安装
npm install -g lighthouse
# 基本运行
lighthouse https://example.com --output html --output-path report.html
# 移动端模拟
lighthouse https://example.com --preset perf --form-factor mobile
# 输出 JSON 格式(便于程序化分析)
lighthouse https://example.com --output json --output-path report.json
# 多次运行取中位数(更准确)
lighthouse https://example.com --output json -n 3
方式三:Node.js API(CI 集成)
typescript
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
interface LighthouseCategory {
score: number;
}
interface LighthouseResult {
categories: {
performance: LighthouseCategory;
accessibility: LighthouseCategory;
'best-practices': LighthouseCategory;
seo: LighthouseCategory;
};
audits: Record<string, { numericValue?: number }>;
}
async function runLighthouse(url: string): Promise<LighthouseResult> {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const options = {
logLevel: 'info' as const,
output: 'json' as const,
port: chrome.port,
onlyCategories: ['performance'],
formFactor: 'mobile' as const,
screenEmulation: {
mobile: true,
width: 375,
height: 812,
deviceScaleFactor: 3,
},
throttling: {
cpuSlowdownMultiplier: 4,
downloadThroughputKbps: 1600,
uploadThroughputKbps: 750,
rttMs: 150,
},
};
const result = await lighthouse(url, options);
await chrome.kill();
const lhr = result?.lhr as LighthouseResult;
return lhr;
}
// 使用示例
async function main(): Promise<void> {
const result = await runLighthouse('https://example.com');
console.log('Performance Score:', result.categories.performance.score * 100);
console.log('FCP:', result.audits['first-contentful-paint']?.numericValue, 'ms');
console.log('LCP:', result.audits['largest-contentful-paint']?.numericValue, 'ms');
console.log('TBT:', result.audits['total-blocking-time']?.numericValue, 'ms');
console.log('CLS:', result.audits['cumulative-layout-shift']?.numericValue);
}
main().catch(console.error);
3.2 Lighthouse 评分解读
Lighthouse Performance 分数由以下指标加权计算:
java
Performance Score 构成 (Lighthouse 12):
+-----------------------------------+--------+
| 指标 | 权重 |
+-----------------------------------+--------+
| First Contentful Paint (FCP) | 10% |
| Largest Contentful Paint (LCP) | 25% |
| Total Blocking Time (TBT) | 30% |
| Cumulative Layout Shift (CLS) | 25% |
| Speed Index (SI) | 10% |
+-----------------------------------+--------+
评分区间:
0-49 红色(差)
50-89 橙色(需要改进)
90-100 绿色(好)
各指标的目标值:
| 指标 | 好(绿色) | 需改进(橙色) | 差(红色) |
|---|---|---|---|
| FCP | <= 1.8s | 1.8s - 3.0s | > 3.0s |
| LCP | <= 2.5s | 2.5s - 4.0s | > 4.0s |
| TBT | <= 200ms | 200ms - 600ms | > 600ms |
| CLS | <= 0.1 | 0.1 - 0.25 | > 0.25 |
| SI | <= 3.4s | 3.4s - 5.8s | > 5.8s |
3.3 常见扣分项与修复方案
以下是 Lighthouse 报告中最常见的扣分项以及对应的修复方案:
3.3.1 Reduce unused JavaScript
问题:打包产物中包含大量未使用的代码。
修复方案:
typescript
// 错误:一次性导入整个库
import _ from 'lodash';
const result = _.get(obj, 'a.b.c');
// 正确:按需导入
import get from 'lodash/get';
const result = get(obj, 'a.b.c');
// 更好:路由级别的代码分割(React 示例)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
3.3.2 Largest Contentful Paint element
问题:LCP 元素(通常是首屏大图或大段文字)加载太慢。
修复方案:
html
<!-- 1. 对 LCP 图片使用 preload -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high" />
<!-- 2. 使用现代图片格式 + 响应式图片 -->
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img
src="hero.jpg"
alt="Hero"
width="1200"
height="600"
fetchpriority="high"
decoding="async"
/>
</picture>
<!-- 3. 确保 LCP 图片不被 lazy loading -->
<!-- 错误:首屏图片不应该 lazy load -->
<img src="hero.jpg" loading="lazy" />
<!-- 正确:首屏图片使用 eager(默认值) -->
<img src="hero.jpg" loading="eager" fetchpriority="high" />
3.3.3 Eliminate render-blocking resources
问题:CSS 和 JS 文件阻塞了首屏渲染。
修复方案:
html
<!-- 关键 CSS 内联到 head 中 -->
<head>
<style>
/* 只放首屏关键样式 */
.hero { display: flex; align-items: center; min-height: 100vh; }
.nav { position: fixed; top: 0; width: 100%; }
</style>
<!-- 非关键 CSS 异步加载 -->
<link
rel="preload"
href="non-critical.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link rel="stylesheet" href="non-critical.css" />
</noscript>
</head>
<!-- 非关键 JS 使用 defer 或 async -->
<script src="main.js" defer></script>
<script src="analytics.js" async></script>
3.3.4 Cumulative Layout Shift 过高
问题:页面元素在加载过程中发生位移。
修复方案:
css
/* 1. 为图片和视频预留空间 */
img, video {
aspect-ratio: 16 / 9; /* 或者明确设置 width 和 height */
width: 100%;
height: auto;
}
/* 2. 为动态插入的内容预留空间 */
.ad-slot {
min-height: 250px; /* 预留广告位高度 */
}
.skeleton {
min-height: 200px; /* 骨架屏预留高度 */
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
typescript
// 3. 避免在已有内容上方动态插入元素
// 错误:在页面顶部插入通知横幅导致内容下移
function showBanner(): void {
const banner = document.createElement('div');
banner.textContent = 'New feature available!';
document.body.prepend(banner); // 会导致所有内容下移 -> CLS
}
// 正确:使用固定定位或预留空间
function showBannerSafe(): void {
const placeholder = document.getElementById('banner-slot');
if (placeholder) {
placeholder.textContent = 'New feature available!';
placeholder.style.display = 'block';
// placeholder 已经在 DOM 中预留了空间,不会导致 CLS
}
}
3.4 在 CI 中集成 Lighthouse
将 Lighthouse 集成到 CI 流程中,可以在每次提交时自动检测性能退化:
typescript
// lighthouse-ci.config.ts
interface LhciConfig {
ci: {
collect: {
url: string[];
numberOfRuns: number;
startServerCommand: string;
};
assert: {
assertions: Record<string, [string, { minScore: number }]>;
};
upload: {
target: string;
serverBaseUrl?: string;
};
};
}
const config: LhciConfig = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
numberOfRuns: 3,
startServerCommand: 'npm run start',
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'first-contentful-paint': ['warn', { minScore: 0 }],
'largest-contentful-paint': ['error', { minScore: 0 }],
'cumulative-layout-shift': ['error', { minScore: 0 }],
'total-blocking-time': ['error', { minScore: 0 }],
},
},
upload: {
target: 'lhci',
serverBaseUrl: 'https://lhci.your-domain.com',
},
},
};
export default config;
配合 GitHub Actions 使用:
yaml
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: './lighthouse-ci.config.ts'
uploadArtifacts: true
temporaryPublicStorage: true
四、Performance API
Performance API 是浏览器提供的原生性能测量接口,可以在生产环境中采集真实用户的性能数据(RUM,Real User Monitoring)。
4.1 Navigation Timing:页面加载各阶段耗时
Navigation Timing API 提供了页面导航过程中每个阶段的精确时间戳。
lua
Navigation Timing 时间线:
navigationStart
|-- redirectStart/End --|
|-- fetchStart --|
|-- domainLookupStart/End --|
|-- connectStart/End --|
|-- requestStart --|
|-- responseStart/End --|
|-- domLoading --|
|-- domInteractive --|
|-- domContentLoaded --|
|-- domComplete --|
|-- loadEventEnd
获取各阶段耗时的代码:
typescript
interface NavigationMetrics {
redirect: number;
dns: number;
tcp: number;
ssl: number;
ttfb: number;
download: number;
domParse: number;
domContentLoaded: number;
domComplete: number;
fullLoad: number;
}
function getNavigationMetrics(): NavigationMetrics | null {
const entries = performance.getEntriesByType('navigation');
if (entries.length === 0) return null;
const nav = entries[0] as PerformanceNavigationTiming;
return {
// 重定向耗时
redirect: nav.redirectEnd - nav.redirectStart,
// DNS 解析耗时
dns: nav.domainLookupEnd - nav.domainLookupStart,
// TCP 连接耗时
tcp: nav.connectEnd - nav.connectStart,
// SSL 握手耗时(HTTPS 页面)
ssl: nav.secureConnectionStart > 0
? nav.connectEnd - nav.secureConnectionStart
: 0,
// TTFB(首字节时间)
ttfb: nav.responseStart - nav.requestStart,
// 内容下载耗时
download: nav.responseEnd - nav.responseStart,
// DOM 解析耗时
domParse: nav.domInteractive - nav.responseEnd,
// DOMContentLoaded 事件耗时
domContentLoaded: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart,
// DOM 完成耗时
domComplete: nav.domComplete - nav.domInteractive,
// 完整页面加载时间
fullLoad: nav.loadEventEnd - nav.startTime,
};
}
// 使用:在页面加载完成后采集
window.addEventListener('load', () => {
// 延迟执行,确保 loadEventEnd 已经有值
setTimeout(() => {
const metrics = getNavigationMetrics();
if (metrics) {
console.table(metrics);
// 上报到你的监控系统
reportToServer('/api/metrics/navigation', metrics);
}
}, 0);
});
4.2 Resource Timing:资源加载分析
Resource Timing API 可以采集页面中每个资源的详细加载时间。
typescript
interface ResourceMetric {
name: string;
type: string;
duration: number;
size: number;
protocol: string;
dns: number;
tcp: number;
ttfb: number;
download: number;
cached: boolean;
}
function getSlowResources(threshold: number = 1000): ResourceMetric[] {
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
return entries
.filter((entry) => entry.duration > threshold)
.map((entry) => ({
name: entry.name.split('/').pop() || entry.name,
type: entry.initiatorType,
duration: Math.round(entry.duration),
size: entry.transferSize,
protocol: entry.nextHopProtocol,
dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart),
tcp: Math.round(entry.connectEnd - entry.connectStart),
ttfb: Math.round(entry.responseStart - entry.requestStart),
download: Math.round(entry.responseEnd - entry.responseStart),
cached: entry.transferSize === 0,
}))
.sort((a, b) => b.duration - a.duration);
}
// 分析资源加载汇总信息
function getResourceSummary(): void {
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
const summary: Record<string, { count: number; totalSize: number; totalDuration: number }> = {};
for (const entry of entries) {
const type = entry.initiatorType;
if (!summary[type]) {
summary[type] = { count: 0, totalSize: 0, totalDuration: 0 };
}
summary[type].count++;
summary[type].totalSize += entry.transferSize;
summary[type].totalDuration += entry.duration;
}
console.table(summary);
}
// 使用
window.addEventListener('load', () => {
setTimeout(() => {
console.log('--- Slow Resources (> 1000ms) ---');
console.table(getSlowResources(1000));
console.log('--- Resource Summary ---');
getResourceSummary();
}, 100);
});
4.3 PerformanceObserver:实时监听性能事件
PerformanceObserver 是现代 Performance API 的核心,它可以异步监听各类性能事件,而不需要轮询。
typescript
// 监听 LCP(Largest Contentful Paint)
function observeLCP(callback: (value: number, element: string) => void): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
// LCP 可能会多次触发,取最后一个值
const lastEntry = entries[entries.length - 1] as PerformanceEntry & {
element?: Element;
};
callback(
lastEntry.startTime,
lastEntry.element?.tagName || 'unknown'
);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
// 监听 FID(First Input Delay)
function observeFID(callback: (value: number, eventType: string) => void): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const fidEntry = entry as PerformanceEntry & {
processingStart: number;
name: string;
};
const delay = fidEntry.processingStart - fidEntry.startTime;
callback(delay, fidEntry.name);
}
});
observer.observe({ type: 'first-input', buffered: true });
}
// 监听 CLS(Cumulative Layout Shift)
function observeCLS(callback: (value: number) => void): void {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const layoutShift = entry as PerformanceEntry & {
hadRecentInput: boolean;
value: number;
};
// 排除用户输入引起的布局偏移
if (layoutShift.hadRecentInput) continue;
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// 如果与上一个条目间隔小于 1 秒,且与会话起始间隔小于 5 秒,
// 则归入当前会话窗口
if (
lastSessionEntry &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
firstSessionEntry &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += layoutShift.value;
} else {
sessionValue = layoutShift.value;
sessionEntries = [];
}
sessionEntries.push(entry);
if (sessionValue > clsValue) {
clsValue = sessionValue;
callback(clsValue);
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
// 监听长任务
function observeLongTasks(callback: (duration: number, attribution: string) => void): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const longTask = entry as PerformanceEntry & {
attribution?: Array<{ containerType?: string; containerName?: string }>;
};
const attr = longTask.attribution?.[0];
const source = attr
? `${attr.containerType || 'unknown'}:${attr.containerName || 'unknown'}`
: 'unknown';
callback(entry.duration, source);
}
});
observer.observe({ type: 'longtask', buffered: true });
}
4.4 自定义性能指标:User Timing API
除了浏览器自动采集的指标,我们还可以用 User Timing API 测量业务逻辑的耗时:
typescript
// 基础用法:mark + measure
function measureDataFetch(): void {
// 标记开始点
performance.mark('data-fetch-start');
fetch('/api/dashboard')
.then((res) => res.json())
.then((data) => {
// 标记结束点
performance.mark('data-fetch-end');
// 创建测量
performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');
// 读取测量结果
const measures = performance.getEntriesByName('data-fetch', 'measure');
console.log(`Data fetch took ${measures[0].duration.toFixed(2)}ms`);
// 清理
performance.clearMarks('data-fetch-start');
performance.clearMarks('data-fetch-end');
performance.clearMeasures('data-fetch');
});
}
// 封装一个通用的性能测量工具
class PerfTracker {
private static instance: PerfTracker;
private observer: PerformanceObserver;
private metrics: Map<string, number[]> = new Map();
private constructor() {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'measure') {
const values = this.metrics.get(entry.name) || [];
values.push(entry.duration);
this.metrics.set(entry.name, values);
}
}
});
this.observer.observe({ type: 'measure', buffered: true });
}
static getInstance(): PerfTracker {
if (!PerfTracker.instance) {
PerfTracker.instance = new PerfTracker();
}
return PerfTracker.instance;
}
// 测量同步函数
measureSync<T>(name: string, fn: () => T): T {
performance.mark(`${name}-start`);
const result = fn();
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
return result;
}
// 测量异步函数
async measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
performance.mark(`${name}-start`);
try {
const result = await fn();
return result;
} finally {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
}
}
// 获取统计数据
getStats(name: string): { avg: number; p50: number; p95: number; count: number } | null {
const values = this.metrics.get(name);
if (!values || values.length === 0) return null;
const sorted = [...values].sort((a, b) => a - b);
const sum = sorted.reduce((a, b) => a + b, 0);
return {
avg: Math.round(sum / sorted.length),
p50: Math.round(sorted[Math.floor(sorted.length * 0.5)]),
p95: Math.round(sorted[Math.floor(sorted.length * 0.95)]),
count: sorted.length,
};
}
// 获取所有指标的汇总
getAllStats(): Record<string, { avg: number; p50: number; p95: number; count: number }> {
const result: Record<string, { avg: number; p50: number; p95: number; count: number }> = {};
for (const name of this.metrics.keys()) {
const stats = this.getStats(name);
if (stats) result[name] = stats;
}
return result;
}
}
// 使用示例
const tracker = PerfTracker.getInstance();
// 测量组件渲染时间
tracker.measureSync('render-user-list', () => {
renderUserList(users);
});
// 测量 API 调用时间
await tracker.measureAsync('fetch-orders', async () => {
const res = await fetch('/api/orders');
return res.json();
});
// 查看统计数据
console.table(tracker.getAllStats());
4.5 性能数据上报
采集到的性能数据需要上报到后端,用于分析和告警。上报时需要注意不影响页面性能:
typescript
interface PerformancePayload {
url: string;
timestamp: number;
userAgent: string;
connection?: string;
metrics: Record<string, number>;
}
class PerformanceReporter {
private buffer: PerformancePayload[] = [];
private readonly endpoint: string;
private readonly batchSize: number;
private readonly flushInterval: number;
private timer: ReturnType<typeof setInterval> | null = null;
constructor(
endpoint: string,
options: { batchSize?: number; flushInterval?: number } = {}
) {
this.endpoint = endpoint;
this.batchSize = options.batchSize || 10;
this.flushInterval = options.flushInterval || 5000;
this.startAutoFlush();
this.setupPageHideFlush();
}
add(metrics: Record<string, number>): void {
const nav = navigator as Navigator & {
connection?: { effectiveType?: string };
};
const payload: PerformancePayload = {
url: location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
connection: nav.connection?.effectiveType,
metrics,
};
this.buffer.push(payload);
if (this.buffer.length >= this.batchSize) {
this.flush();
}
}
private flush(): void {
if (this.buffer.length === 0) return;
const data = [...this.buffer];
this.buffer = [];
// 使用 sendBeacon 上报,不阻塞页面卸载
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const sent = navigator.sendBeacon(this.endpoint, blob);
// sendBeacon 失败时降级为 fetch
if (!sent) {
fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
}).catch(() => {
// 静默失败,不影响用户体验
});
}
}
private startAutoFlush(): void {
this.timer = setInterval(() => this.flush(), this.flushInterval);
}
private setupPageHideFlush(): void {
// 页面隐藏或卸载时立即上报
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
destroy(): void {
if (this.timer) {
clearInterval(this.timer);
}
this.flush();
}
}
// 使用示例
const reporter = new PerformanceReporter('/api/performance', {
batchSize: 5,
flushInterval: 10000,
});
// 采集 Web Vitals 并上报
observeLCP((value) => {
reporter.add({ lcp: Math.round(value) });
});
observeFID((value) => {
reporter.add({ fid: Math.round(value) });
});
observeCLS((value) => {
reporter.add({ cls: value });
});
observeLongTasks((duration) => {
reporter.add({ longTaskDuration: Math.round(duration) });
});
五、实战工作流:三件套组合使用
前面分别介绍了每个工具的用法,下面用一个具体场景演示如何将它们串联起来形成完整的优化闭环。
场景描述
某电商网站首页加载缓慢,用户反馈从点击到看到商品列表需要等待较长时间。需要定位问题、优化并建立持续监控。
第一步:DevTools 定位问题
markdown
排查流程:
1. 打开无痕模式 + DevTools
2. 切换到 Performance 面板,设置 4x CPU + Fast 3G
3. 录制页面加载过程
4. 分析结果
发现的问题:
[问题 A] 火焰图显示 main.js 执行耗时 1200ms(长任务)
|-- 展开发现是 JSON.parse 处理一个 2MB 的商品数据
[问题 B] Network 面板发现 hero-image.png 体积 3.2MB,
TTFB 正常但 Content Download 耗时 4.1s
[问题 C] Performance 面板出现紫色 Layout 块(Forced Reflow),
定位到商品卡片的渲染函数
第二步:Lighthouse 量化问题
运行 Lighthouse 得到基线分数:
yaml
Lighthouse 基线报告:
Performance Score: 42
---------------------------------
FCP: 2.8s (橙色)
LCP: 6.2s (红色) <-- 主要由 hero 大图导致
TBT: 890ms (红色) <-- main.js 长任务导致
CLS: 0.32 (红色) <-- 商品卡片渲染导致
SI: 5.1s (橙色)
关键建议:
- Properly size images (预估节省 3.1s)
- Reduce unused JavaScript (预估节省 0.8s)
- Avoid large layout shifts (CLS 改善)
第三步:逐个修复
typescript
// 修复问题 A:将大数据处理移到 Web Worker
// worker.ts
self.addEventListener('message', (e: MessageEvent<string>) => {
const data = JSON.parse(e.data);
// 在 Worker 中执行耗时的数据处理
const processed = data.products.map((p: { id: string; name: string; price: number }) => ({
id: p.id,
name: p.name,
price: p.price,
}));
self.postMessage(processed);
});
// main.ts
async function loadProducts(): Promise<void> {
const response = await fetch('/api/products');
const text = await response.text();
const worker = new Worker(new URL('./worker.ts', import.meta.url));
return new Promise((resolve) => {
worker.onmessage = (e) => {
renderProducts(e.data);
worker.terminate();
resolve();
};
worker.postMessage(text);
});
}
typescript
// 修复问题 B:图片优化
// next.config.js(Next.js 示例)或手动配置
// 1. 将 PNG 转为 WebP/AVIF
// 2. 使用响应式图片
// 3. 实现图片 CDN 处理
function getOptimizedImageUrl(
src: string,
width: number,
format: 'webp' | 'avif' = 'webp'
): string {
// 假设使用图片 CDN 服务
return `https://cdn.example.com/resize?src=${encodeURIComponent(src)}&w=${width}&fmt=${format}&q=80`;
}
// 实现渐进式加载:先显示低质量占位图,再加载高清图
function createProgressiveImage(src: string, container: HTMLElement): void {
// 先加载低质量的模糊占位图(约 1-2KB)
const placeholder = document.createElement('img');
placeholder.src = getOptimizedImageUrl(src, 40, 'webp');
placeholder.style.filter = 'blur(20px)';
placeholder.style.width = '100%';
placeholder.style.aspectRatio = '16/9';
container.appendChild(placeholder);
// 异步加载高清图
const fullImage = new Image();
fullImage.src = getOptimizedImageUrl(src, 1200, 'webp');
fullImage.onload = () => {
placeholder.src = fullImage.src;
placeholder.style.filter = 'none';
};
}
typescript
// 修复问题 C:消除强制回流
// 优化前:每张商品卡片渲染时都在读写交替
function renderProductCardBad(product: { name: string; image: string }): void {
const card = document.createElement('div');
card.className = 'product-card';
card.innerHTML = `<img src="${product.image}" /><h3>${product.name}</h3>`;
document.getElementById('product-list')!.appendChild(card);
// 读取布局属性 -> 强制回流
const cardHeight = card.offsetHeight;
card.style.minHeight = `${cardHeight}px`;
}
// 优化后:使用 CSS 固定尺寸 + 批量 DOM 操作
function renderProductCards(products: { name: string; image: string }[]): void {
// 使用 DocumentFragment 批量操作 DOM
const fragment = document.createDocumentFragment();
for (const product of products) {
const card = document.createElement('div');
card.className = 'product-card'; // CSS 中已定义固定高度
card.innerHTML = `
<img src="${product.image}" width="280" height="210" loading="lazy" />
<h3>${product.name}</h3>
`;
fragment.appendChild(card);
}
// 一次性挂载到 DOM
document.getElementById('product-list')!.appendChild(fragment);
}
第四步:用 Lighthouse 验证优化效果
yaml
Lighthouse 优化后报告:
Performance Score: 91 (+49)
---------------------------------
FCP: 1.2s (绿色) +1.6s 提升
LCP: 2.1s (绿色) +4.1s 提升
TBT: 120ms (绿色) +770ms 提升
CLS: 0.05 (绿色) +0.27 提升
SI: 2.8s (绿色) +2.3s 提升
第五步:Performance API 建立生产监控
typescript
// performance-monitor.ts
// 完整的生产环境性能监控方案
interface WebVitals {
lcp?: number;
fid?: number;
cls?: number;
fcp?: number;
ttfb?: number;
inp?: number;
}
function initPerformanceMonitor(reportEndpoint: string): void {
const vitals: WebVitals = {};
const reporter = new PerformanceReporter(reportEndpoint);
// 1. 采集 LCP
observeLCP((value) => {
vitals.lcp = Math.round(value);
});
// 2. 采集 FID
observeFID((value) => {
vitals.fid = Math.round(value);
});
// 3. 采集 CLS
observeCLS((value) => {
vitals.cls = parseFloat(value.toFixed(4));
});
// 4. 采集 FCP
const fcpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
vitals.fcp = Math.round(entry.startTime);
}
}
});
fcpObserver.observe({ type: 'paint', buffered: true });
// 5. 采集 TTFB
const navEntries = performance.getEntriesByType('navigation');
if (navEntries.length > 0) {
const nav = navEntries[0] as PerformanceNavigationTiming;
vitals.ttfb = Math.round(nav.responseStart - nav.requestStart);
}
// 6. 采集 INP(Interaction to Next Paint)
let maxINP = 0;
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const eventEntry = entry as PerformanceEntry & {
processingStart: number;
processingEnd: number;
};
const duration = entry.duration;
if (duration > maxINP) {
maxINP = duration;
vitals.inp = Math.round(duration);
}
}
});
inpObserver.observe({ type: 'event', buffered: true });
// 7. 在页面隐藏时上报所有指标
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
reporter.add(vitals as Record<string, number>);
}
});
// 8. 监控长任务并告警
observeLongTasks((duration, attribution) => {
if (duration > 200) {
// 超过 200ms 的长任务触发告警
reporter.add({
longTaskDuration: Math.round(duration),
longTaskSource: attribution as unknown as number,
});
console.warn(`[Performance] Long task detected: ${duration}ms from ${attribution}`);
}
});
// 9. 监控慢接口
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const resource = entry as PerformanceResourceTiming;
if (
resource.initiatorType === 'fetch' ||
resource.initiatorType === 'xmlhttprequest'
) {
if (resource.duration > 3000) {
reporter.add({
slowApiUrl: resource.name as unknown as number,
slowApiDuration: Math.round(resource.duration),
slowApiTtfb: Math.round(resource.responseStart - resource.requestStart),
});
}
}
}
});
resourceObserver.observe({ type: 'resource', buffered: false });
}
// 应用入口调用
initPerformanceMonitor('/api/rum/collect');
三件套组合流程图
lua
+================================================================+
| 性能优化完整工作流 |
+================================================================+
开发阶段 发布前 生产环境
-------- ------ --------
DevTools Performance Lighthouse CI Performance API
DevTools Network 性能门禁 RUM 监控
| | |
v v v
+------------+ +------------+ +------------+
| 录制分析 | | 自动跑分 | | 实时采集 |
| 火焰图 | | 基准对比 | | Web Vitals |
| 瀑布流 | | PR 评论 | | 自定义指标 |
| 强制回流 | | 阈值告警 | | 上报存储 |
+------------+ +------------+ +------------+
| | |
+------ 发现问题 -----------+------ 量化验证 ---------+
|
v
+----------------+
| 定位根因 |
| 实施优化 |
| 验证效果 |
| 持续监控 |
+----------------+
以下是三者的定位对比表:
| 维度 | DevTools | Lighthouse | Performance API |
|---|---|---|---|
| 使用阶段 | 开发 / 调试 | 开发 / CI | 生产环境 |
| 数据类型 | 实验室数据 | 实验室数据 | 真实用户数据(RUM) |
| 主要用途 | 定位具体性能瓶颈 | 量化评分 + 优化建议 | 持续监控 + 告警 |
| 粒度 | 函数级别 / 请求级别 | 页面级别 | 可自定义 |
| 自动化 | 手动操作 | 可 CI 集成 | 自动采集上报 |
| 优势 | 深入定位根因 | 标准化评分,易于沟通 | 反映真实用户体验 |
| 局限 | 只能代表本地环境 | 模拟环境可能与生产有差异 | 需要自建采集和分析系统 |
六、进阶技巧
6.1 使用 web-vitals 库简化采集
Google 维护的 web-vitals 库封装了 Web Vitals 的采集逻辑,比手写 PerformanceObserver 更健壮:
typescript
import { onLCP, onFID, onCLS, onFCP, onTTFB, onINP } from 'web-vitals';
import type { Metric } from 'web-vitals';
function sendToAnalytics(metric: Metric): void {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta, // 自上次报告以来的变化量
id: metric.id, // 唯一标识(用于去重)
navigationType: metric.navigationType,
url: location.href,
};
// 使用 sendBeacon 上报
navigator.sendBeacon('/api/vitals', JSON.stringify(body));
}
// 一行代码搞定所有 Web Vitals 采集
onLCP(sendToAnalytics);
onFID(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
onINP(sendToAnalytics);
6.2 DevTools 中的 Performance Insights 面板
Chrome 新推出的 Performance Insights 面板(Ctrl+Shift+I -> Performance Insights)提供了比传统 Performance 面板更易读的分析视图:
- 自动标注 LCP、FCP、CLS 等关键指标
- 以交互时间线的形式展示,而非原始火焰图
- 自动关联 Insight 建议(如"此长任务阻塞了交互")
这个面板特别适合快速诊断,而传统 Performance 面板适合深入分析。
6.3 Chrome DevTools Protocol 自动化
对于需要批量分析的场景,可以通过 Chrome DevTools Protocol 实现自动化:
typescript
import puppeteer from 'puppeteer';
interface TraceEvent {
cat: string;
name: string;
dur?: number;
}
async function automatedPerfAudit(url: string): Promise<void> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox'],
});
const page = await browser.newPage();
// 模拟移动端
await page.emulate(puppeteer.KnownDevices['iPhone 12']);
// 开启 tracing(等同于 Performance 面板录制)
await page.tracing.start({
screenshots: true,
categories: ['devtools.timeline'],
});
// 导航并等待加载完成
await page.goto(url, { waitUntil: 'networkidle0' });
// 停止 tracing
const traceBuffer = await page.tracing.stop();
const traceData = JSON.parse(traceBuffer.toString());
// 分析长任务
const longTasks = traceData.traceEvents
.filter(
(e: TraceEvent) =>
e.cat === 'devtools.timeline' &&
e.name === 'RunTask' &&
e.dur &&
e.dur > 50000 // 微秒,50ms = 50000us
)
.map((e: TraceEvent) => ({
duration: (e.dur! / 1000).toFixed(1) + 'ms',
}));
console.log('Long Tasks:', longTasks);
// 提取资源加载信息
const resources = await page.evaluate(() => {
return performance
.getEntriesByType('resource')
.map((r) => {
const resource = r as PerformanceResourceTiming;
return {
name: resource.name.split('/').pop(),
duration: Math.round(resource.duration),
size: resource.transferSize,
};
})
.sort((a, b) => b.duration - a.duration)
.slice(0, 10);
});
console.log('Top 10 Slowest Resources:', resources);
await browser.close();
}
automatedPerfAudit('https://example.com').catch(console.error);
总结
Chrome DevTools、Lighthouse、Performance API 三者各有侧重,分别解决性能优化链路中不同阶段的需求:
-
DevTools 是「显微镜」,用于在开发阶段深入定位性能瓶颈,精确到函数调用和单个网络请求。掌握火焰图、长任务识别和瀑布流分析是基本功。
-
Lighthouse 是「体检报告」,用标准化的评分体系量化页面性能,提供可操作的优化建议。集成到 CI 后可以充当性能门禁,防止性能退化。
-
Performance API 是「监控探针」,部署到生产环境后持续采集真实用户的性能数据。实验室数据再好看,也不如 RUM 数据真实。
三者的最佳实践是组合使用:用 DevTools 发现和定位问题,用 Lighthouse 量化优化效果并设置门禁,用 Performance API 在生产环境中持续监控。性能优化不是一次性的工作,而是一个需要持续关注的过程。
如果觉得有帮助,欢迎点赞收藏关注,后续会继续分享前端性能优化相关的实践。