性能优化排查指南

一、排查思路总览

性能问题排查应遵循从宏观到微观的路径:先确认真实用户数据,再在开发环境复现,最后用工具定位根因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户反馈慢

├── 是否有真实用户数据?
│ ├── 有 → 看 RUM(Real User Monitoring)指标
│ └── 无 → 搭建 Lighthouse / Web Vitals 采集

├── 是加载慢还是交互卡?
│ ├── 加载慢 → 看 Network + 关键渲染路径
│ └── 交互卡 → 看 Performance + FPS

├── 是 CPU 问题还是网络问题?
│ ├── CPU → 看 Performance火焰图 + Memory
│ └── 网络 → 看 Network 瀑布图

└── 是框架问题还是浏览器问题?
├── 框架 → React DevTools / Vue DevTools
└── 浏览器 → Chrome DevTools 原生面板

二、工具使用

Chrome DevTools 核心面板

Performance 面板(火焰图)

用于分析运行时性能,定位JS执行耗时和渲染瓶颈。

1
2
3
4
5
操作步骤:
1. 打开 DevTools → Performance
2. 点击录制按钮(或 Ctrl+E)
3. 在页面上执行需要分析的操作(点击、滚动等)
4. 停止录制,分析火焰图

火焰图中需要关注的关键区域:

区域颜色代表含义优化方向
JavaScript黄色函数调用栈减少执行时间、拆分任务
Style / Layout紫色样式计算+布局减少选择器复杂度、批量DOM
Paint绿色绘制使用合成层、减少重绘区域
System灰色系统调度减少任务数量
Idle无色空闲越多越好

排查步骤

1
2
3
4
1. 找到长任务(红色三角形标记 Task)
2. 点击查看调用栈 → 定位耗时函数
3. 观察 Layout 和 Paint 标记 → 确认是否频繁回流
4. 查看 Summary 面板 → 判断瓶颈类型(JS/渲染/绘制)

Network 面板(网络瀑布图)

用于分析资源加载耗时。

1
2
3
4
5
操作步骤:
1. 打开 DevTools → Network
2. 勾选 "Disable cache"(模拟首次访问)
3. 刷新页面
4. 观察瀑布图

需要关注的指标:

指标说明优化方向
DNS LookupDNS 解析时间preconnect / dns-prefetch
TCP ConnectTCP 连接时间使用 HTTP/2 / 复用连接
TTFB首字节时间服务端优化、CDN
Content Download内容下载时间压缩、CDN、缓存
Waterfall 串行请求排队等待并行请求、HTTP/2 多路复用

排查步骤

1
2
3
4
5
6
7
8
1. 查看是否有请求排队(Waterfall 前端灰色等待)
→ 同一域名连接数限制 → 域名分片或 HTTP/2
2. 查看 TTFB 是否过长
→ 服务端慢 → 后端优化或 CDN 缓存
3. 查看大文件下载时间
→ JS/CSS/图片过大 → 压缩/分割/懒加载
4. 查看是否有关键 CSS/JS 加载过晚
→ 影响首屏 → preload / 内联

Memory 面板(内存分析)

用于排查内存泄漏和内存占用过高。

1
2
3
4
5
6
操作步骤:
1. 打开 DevTools → Memory
2. 选择 Heap Snapshot(堆快照)
3. 先拍一张作为基准
4. 执行需要分析的操作(打开/关闭页面、反复操作)
5. 再拍一张,对比 Diff

排查步骤

1
2
3
4
5
6
7
1. 切换至 Heap Snapshot 面板
2. 选择 Comparison 视图(与基准快照对比)
3. 过滤出 # New(新增对象)
4. 检查 Detached DOM Tree 节点
→ 组件卸载后 DOM 仍被引用 → 内存泄漏
5. 分析 retainers(引用链)
→ 找到谁持有了这些对象

Lighthouse(综合审计)

用于生成性能评分报告和优化建议。

1
2
3
4
5
操作步骤:
1. 打开 DevTools → Lighthouse
2. 选择要审计的类别(Performance / Accessibility / SEO)
3. 点击 Generate report
4. 查看评分和建议

性能 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 1. 精确测量代码段耗时
performance.mark('start');
heavyFunction();
performance.mark('end');
performance.measure('heavyFunction', 'start', 'end');
const measure = performance.getEntriesByName('heavyFunction')[0];
console.log(`${measure.duration}ms`);

// 2. 获取 FPS
let frameCount = 0;
let lastTime = performance.now();

function measureFPS() {
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
console.log(`FPS: ${frameCount}`);
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(measureFPS);
}
measureFPS();
// FPS < 30 表示严重卡顿,FPS 30-55 可接受,FPS 55-60 流畅

// 3. 获取导航时间
const perfData = performance.getEntriesByType('navigation')[0];
console.log({
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
tcp: perfData.connectEnd - perfData.connectStart,
ttfb: perfData.responseStart - perfData.requestStart,
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart,
load: perfData.loadEventEnd - perfData.navigationStart,
});

// 4. 观察布局偏移(CLS)
let cumulativeCLS = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cumulativeCLS += entry.value;
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });

三、分场景排查流程

场景 1:首屏加载过慢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
排查流程:
1. 打开 Network 面板,Disable cache,刷新页面
2. 观察 TTFB 是否过长(> 500ms)

├── 是 → 后端接口慢或 CDN 未命中
│ └── 确认后端接口耗时 / 确认 CDN 缓存策略

├── 否 → 查看关键渲染路径
│ ├── 查看 HTML 中是否有阻塞渲染的 CSS 或 JS
│ │ ├── CSS 阻塞 → 内联关键 CSS / 异步加载非关键 CSS
│ │ └── JS 阻塞 → 添加 defer / async / script 放在 body 末尾
│ │
│ ├── 查看 Largest Contentful Paint(LCP)是什么
│ │ ├── 图片 → preload + 压缩 + 响应式
│ │ ├── 文字 → 字体 font-display: swap
│ │ └── 大组件 → 代码分割 + 骨架屏
│ │
│ └── 查看请求瀑布图是否有串行等待
│ ├── 有 → 改为并行请求 / HTTP/2 / 域名分片
│ └── 无 → 进一步分析 JS 解析时间

└── 观察 JS Bundle 大小
├── > 200KB → 代码分割 / Tree Shaking
└── > 500KB → 重点优化:动态加载路由级组件

场景 2:页面交互卡顿(点击/输入/滚动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
排查流程:
1. 打开 Performance 面板,点击录制
2. 在页面上执行卡顿的操作
3. 停止录制,观察火焰图

├── 有没有长任务(Long Task)?
│ ├── 有 → 点击查看调用栈
│ │ ├── 框架渲染函数(React render / Vue patch)→ 组件重渲染过多
│ │ ├── 数据处理(map/sort/filter)→ Web Worker / 分批处理
│ │ └── 第三方库(moment/ lodash)→ 替换轻量库 / 按需导入
│ │
│ └── 无 → 看下一级

├── 有没有频繁 Layout / Recalc Style 标记?
│ ├── 有 → 哪些属性在变化
│ │ ├── left / top / width → 改为 transform
│ │ ├── 读取 offsetHeight / scrollTop 后修改样式 → 分开读写
│ │ └── 滚动事件中触发 DOM 操作 → 防抖节流
│ │
│ └── 无 → 看下一级

└── 有没有频繁 Paint 标记?
├── 有 → 动画元素是否触发重绘
│ └── 使用 will-change: transform / 提升合成层

└── 无 → 测量 FPS
├── FPS < 30 → 减少动画复杂度 / 降低帧率
└── FPS 30+ → 可能是网络问题导致的感知卡顿

场景 3:数据列表滚动卡顿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
排查流程:
1. 打开 Console,执行 console.log 查看列表渲染了多少个 DOM

├── DOM 数量 > 5000 → 虚拟滚动
│ └── 检查列表项中的事件绑定
│ ├── 每个列表项有独立事件 → 事件委托
│ └── 列表项内有复杂子组件 → 减少嵌套组件

├── DOM 数量 1000-5000 → 分批渲染
│ └── 每帧渲染 20-50 个,用 requestAnimationFrame 控制

└── DOM 数量 < 1000 → 问题不在 DOM 数量
├── 检查每个列表项的渲染耗时
│ └── 使用 React.memo / Vue computed 避免不必要重渲染

└── 检查父组件是否每次滚动都重渲染全部列表
└── 分离不变部分和可变部分

场景 4:内存持续增长(内存泄漏)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
排查流程:
1. 打开 Memory 面板,拍一张 Heap Snapshot
2. 反复执行可能泄漏的操作(打开/关闭弹窗、切换页面)
3. 拍第二张 Heap Snapshot,选择 Comparison 视图

├── Detached DOM Tree 数量增加了?
│ ├── 是 → DOM 元素被 JS 引用,但已从 DOM 树移除
│ │ └── 检查全局变量、闭包、事件监听中是否有 DOM 引用未释放
│ │
│ └── 否 → 看下一个

├── 数组/对象数量持续增长?
│ ├── 是 → 检查定时器/订阅/全局状态
│ │ ├── setInterval 未 clear → 组件卸载时清除
│ │ ├── addEventListener 未 remove → 组件卸载时移除
│ │ └── 全局状态(Redux/Pinia)不断追加不清理 → 限制数据量
│ │
│ └── 否 → 看下一个

└── 使用 Performance → Memory 录制一段时间
├── JS Heap 持续上升不下降 → 泄漏
├── JS Heap 有升有降但整体稳定 → 正常
└── JS Heap 锯齿形但总体上升 → 泄漏

场景 5:组件重渲染过多(React/Vue 特有)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
排查流程:
1. 安装 React DevTools / Vue DevTools
2. 打开 Components / Profiler 面板

├── React: 启用 Highlight Updates
│ ├── 点击/输入时是否有大量组件高亮?
│ │ ├── 是 → 检查哪些组件的 props 没变但重新渲染了
│ │ │ ├── 未使用 React.memo → 添加 memo
│ │ │ ├── 内联函数/对象每次重建 → useCallback / useMemo
│ │ │ └── Context value 每次新建 → useMemo 包裹 value
│ │ │
│ │ └── 否 → 看 Profiler 火焰图
│ │
│ └── React: 打开 Profiler → 录制操作
│ └── 查看组件渲染耗时 → 优化最耗时的组件

└── Vue: 打开 Vue DevTools → Performance
├── 启用组件渲染追踪
├── 查看组件渲染次数和时间
└── 查找不必要的重新渲染
├── 未使用 computed → 需计算时用 computed 缓存
├── 未使用 v-memo → 列表中使用 v-memo 跳过不变项
└── props 包含对象时每次重新创建 → 稳定引用

场景 6:打包产物过大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
排查流程:
1. 使用 Vite/Webpack 的可视化分析工具

├── Vite: npx vite-bundle-analyzer
├── Webpack: npx webpack-bundle-analyzer
└── 查看分析图

├── 最大的 chunk 是什么?
│ ├── 三方库(lodash/moment/echarts)→
│ │ ├── 用 lodash-es / dayjs 替代
│ │ ├── externals CDN 加载
│ │ └── 动态 import(仅在需要时加载)
│ │
│ └── 业务代码
│ ├── 路由级代码分割 → React.lazy / defineAsyncComponent
│ └── 是否有不需要的模块被打包进来
│ └── Tree Shaking 未生效 → 检查 sideEffects

├── 有没有重复打包的库?
│ └── yarn deduplicate / npm dedupe

└── 有没有很大的 JSON 或图片被打包进 JS?
└── JSON 单独加载 / 图片用 URL 而非 import

四、线上监控方案

RUM(Real User Monitoring)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 web-vitals 采集核心指标
import { onLCP, onFID, onCLS, onTTFB, onINP } from 'web-vitals';

function sendToAnalytics(metric) {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta,
id: metric.id,
url: window.location.href,
userAgent: navigator.userAgent,
};
// 发送到自己的分析服务
navigator.sendBeacon('/api/metrics', JSON.stringify(body));
}

onLCP(sendToAnalytics);
onFID(sendToAnalytics);
onCLS(sendToAnalytics);
onTTFB(sendToAnalytics);
onINP(sendToAnalytics); // Interaction to Next Paint(2024 新指标)

性能预算

在 CI/CD 中设置性能预算,超过阈值则构建失败:

1
2
3
4
5
6
7
8
{
"performance-budget": {
"maxBundleSize": 200000,
"maxLCP": 2500,
"maxTBT": 300,
"maxCLS": 0.1
}
}

关键指标预警值

指标良好需改善
LCP≤ 2.5s2.5s - 4.0s> 4.0s
FID (INP)≤ 200ms200ms - 500ms> 500ms
CLS≤ 0.10.1 - 0.25> 0.25
TTFB≤ 800ms800ms - 1.8s> 1.8s
FPS55-6030-55< 30
JS Bundle< 200KB200KB - 500KB> 500KB