一、排查思路总览
性能问题排查应遵循从宏观到微观的路径:先确认真实用户数据,再在开发环境复现,最后用工具定位根因。
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 原生面板
|
二、工具使用
用于分析运行时性能,定位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 Lookup | DNS 解析时间 | preconnect / dns-prefetch |
| TCP Connect | TCP 连接时间 | 使用 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
| performance.mark('start'); heavyFunction(); performance.mark('end'); performance.measure('heavyFunction', 'start', 'end'); const measure = performance.getEntriesByName('heavyFunction')[0]; console.log(`${measure.duration}ms`);
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();
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, });
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
| import { onLCP, onFID, onCLS, onTTFB, onINP } from 'web-vitals';
function sendToAnalytics(metric) { const body = { name: metric.name, value: metric.value, rating: metric.rating, 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);
|
性能预算
在 CI/CD 中设置性能预算,超过阈值则构建失败:
1 2 3 4 5 6 7 8
| { "performance-budget": { "maxBundleSize": 200000, "maxLCP": 2500, "maxTBT": 300, "maxCLS": 0.1 } }
|
关键指标预警值
| 指标 | 良好 | 需改善 | 差 |
|---|
| LCP | ≤ 2.5s | 2.5s - 4.0s | > 4.0s |
| FID (INP) | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
| TTFB | ≤ 800ms | 800ms - 1.8s | > 1.8s |
| FPS | 55-60 | 30-55 | < 30 |
| JS Bundle | < 200KB | 200KB - 500KB | > 500KB |