关于 performance-profiling
面向开发者的性能分析技能,指导先测量再优化,涵盖 Node.js、React、浏览器和数据库查询,并提供结果解读与瓶颈分类方法。
主要功能
- 01Node.js CPU 与内存分析
- 02火焰图分析
- 03React 渲染性能分析
- 04浏览器性能诊断
- 05数据库查询分析
- 06瓶颈分类指导
适用场景
- 01定位 Node.js 应用的 CPU 热点
- 02排查堆增长和内存泄漏
- 03发现不必要的 React 重渲染
- 04分析缓慢的数据库查询
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill performance-profiling安装后在 Claude Code 中输入/performance-profiling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/browser-profiling.mdnode-profiling.md详细介绍
性能分析
在修改任何代码行之前,先找出应用实际耗时的地方。涵盖完整技术栈:Node.js CPU 和内存分析、浏览器火焰图、React 渲染分析,以及数据库查询分析。这里的原则是先分析,再优化——过早优化不是工作流,而是猜测。
适用场景
适用于:
- 诊断运行缓慢的 Node.js 应用(受 CPU 限制、受 I/O 限制、内存压力)
- 生成并阅读火焰图,查找热点代码路径
- 通过堆快照和增长趋势检测内存泄漏
- 使用 React Profiler 分析 React 组件渲染性能
- 衡量浏览器渲染性能(Core Web Vitals、布局抖动、长任务)
- 使用 EXPLAIN ANALYZE 分析数据库查询
- 衡量事件循环利用率和延迟
不适用于:
- 基础设施监控、分布式追踪或日志聚合(使用
logging-observability) - 负载测试和容量规划(属于单独的领域)
- 服务间网络延迟分析(使用分布式追踪工具)
- 数据库架构设计优化(与查询分析不同)
核心判断:我的应用在哪里变慢了?
flowchart TD
Start[App is slow. Where?] --> Layer{Which layer?}
Layer -->|Backend| Backend{What kind?}
Layer -->|Frontend/browser| Browser{What symptom?}
Layer -->|Unknown| Measure[Instrument first — add timing logs]
Backend -->|CPU pegged, slow responses| CPU[CPU Profiling]
Backend -->|Memory growing, crashes| Mem[Memory / Heap Profiling]
Backend -->|Fast CPU, slow I/O| IO{I/O type?}
IO -->|Database queries| DB[EXPLAIN ANALYZE + query profiler]
IO -->|Network calls| Network[Trace external calls, add timeouts]
IO -->|File system| FS[Check event loop utilization]
Browser -->|Slow initial load| Lighthouse[Lighthouse + bundle analysis]
Browser -->|Janky scrolling, animations| Rendering[Chrome Performance tab — layout thrashing]
Browser -->|Slow after interaction| React{React app?}
React -->|Yes| ReactProfiler[React Profiler + why-did-you-render]
React -->|No| JS[Chrome Performance — long tasks, main thread blocking]
CPU --> FlameGraph[Generate flame graph with 0x or clinic flame]
Mem --> HeapSnap[Take heap snapshots before/after suspected leak]
FS --> ELU[clinic bubbles — event loop utilization]
Node.js:CPU 分析
V8 Inspector(内置)
# Attach inspector and capture a CPU profile
node --inspect src/index.js
# Or start paused and wait for DevTools
node --inspect-brk src/index.js
然后在 Chrome 中打开 chrome://inspect,点击目标,进入 Profiler 标签页,并在向服务器发送负载时录制。
0x:在终端生成火焰图
npm install -g 0x
# Profile a script (runs it, generates flame graph)
0x -- node src/index.js
# Profile with a load generator running simultaneously
0x -- node src/server.js &
npx autocannon -d 30 http://localhost:3000/api/heavy
0x 会生成交互式 HTML 火焰图。最宽的堆栈就是耗时所在。请留意:
- 底部附近较宽的函数(被各种函数频繁调用)
- 库代码中异常宽的部分(序列化、模板引擎、解析器)
- 空闲 /
[idle]区块——这是 I/O 等待,不是 CPU(请在别处查找)
Clinic.js 套件
npm install -g clinic
# Doctor: overview of what is wrong
clinic doctor -- node src/server.js
# Flame: CPU flame graph (wraps 0x)
clinic flame -- node src/server.js
# Bubbles: event loop utilization
clinic bubbles -- node src/server.js
Clinic Doctor 会提供分诊视图,展示 CPU、内存、事件循环和句柄。如果不清楚瓶颈类型,请从这里开始。
事件循环利用率(ELU)
const { performance } = require('perf_hooks');
// Sample ELU every 5 seconds
let last = performance.eventLoopUtilization();
setInterval(() => {
const current = performance.eventLoopUtilization();
const diff = performance.eventLoopUtilization(current, last);
console.log(`ELU: ${(diff.utilization * 100).toFixed(1)}%`);
last = current;
}, 5000);
ELU 超过 80% 表示事件循环已饱和——工作受 CPU 限制或存在同步阻塞。响应缓慢而 ELU 接近 0%,则表示在等待 I/O(网络、磁盘、数据库)。
Node.js:内存分析
堆快照
# Take heap snapshot via CLI
node --inspect src/index.js
# In chrome://inspect → Memory tab → Take Heap Snapshot
用于检测泄漏的三快照法:
- 启动后拍摄快照(基线)
- 处理 N 个请求后拍摄快照(预热后)
- 处理 2N 个请求后拍摄快照(增长后)
比较快照 3 和快照 2——随请求数量成比例增长的对象就是泄漏对象。
常见泄漏模式
闭包捕获——长期存活的闭包捕获了本应释放的变量:
// LEAK: handler is registered but never removed
emitter.on('data', (chunk) => {
processedData.push(chunk); // processedData grows unbounded
});
// FIX: remove listener when done, or use once()
emitter.once('data', handler);
// or
const handler = (chunk) => { ... };
emitter.on('data', handler);
// later:
emitter.off('data', handler);
不断增长且没有淘汰机制的缓存:
// LEAK: cache grows forever
const cache = new Map();
app.get('/user/:id', async (req, res) => {
if (!cache.has(req.params.id)) {
cache.set(req.params.id, await db.getUser(req.params.id));
}
res.json(cache.get(req.params.id));
});
// FIX: use LRU cache with max size
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 });
WeakRef 和 FinalizationRegistry(用于有意创建弱引用):
const cache = new Map();
function cacheValue(key, obj) {
const ref = new WeakRef(obj);
const registry = new FinalizationRegistry((k) => cache.delete(k));
registry.register(obj, key);
cache.set(key, ref);
}
反模式:不做分析就优化
新手:“这个函数看起来开销很大,我要用更高效的算法重写它。”
专家:结果重写了错误的函数。分析本会表明,这个函数每次启动只调用一次,仅占运行时间的 0.1%。真正的瓶颈是响应处理程序中的 JSON 序列化,每秒调用 10,000 次。优化工作必须依据测量结果,而不是直觉。
识别方式:“优化后”的代码在微基准测试中确实更快,但生产环境中的 p99 延迟没有变化。
反模式:孤立地进行微基准测试
新手:编写基准测试,比较两个排序算法对包含 1000 个元素的数组进行排序的性能,得出算法 B 快 2 倍的结论,然后重写生产代码。
专家:微基准测试是在人工条件下测量经过 JIT 编译的热点路径。实际工作负载的数据形态各异,调用模式混杂,还会有 GC 压力和交错的 I/O。JIT 可能会以不同于实际调用位置的方式优化基准测试。应在真实负载下分析实际应用;或者至少,在实际应用代码路径中使用符合实际情况的数据形态和调用模式进行分析。
测试方法:你的基准测试是否在开始测量前,在紧密循环中运行 10,000 次?如果是,V8 对它进行 JIT 编译的方式就会不同于真实代码;真实代码在启动时是冷态的,而且接收的输入各不相同。
React 渲染性能
React Profiler(DevTools)
- 打开 React DevTools → Profiler 选项卡
- 点击“Record”
- 执行导致性能缓慢的交互
- 停止录制
- 查看火焰图——条形代表组件,宽度代表渲染时间
关键列:“Why did this render?” 会显示触发每次渲染的 prop 或 state 变更。
why-did-you-render
npm install @welldone-software/why-did-you-render
// src/wdyr.js (import before React)
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: true });
}
// Mark a specific component for tracking
MyExpensiveComponent.whyDidYouRender = true;
每当组件在 props 未变的情况下重新渲染时,此工具都会向控制台记录日志,从而揭示因引用相等性判断失败而导致的不必要渲染。
常见的 React 性能模式
// Memoize expensive components
const ExpensiveList = React.memo(({ items, onSelect }) => {
return items.map(item => <Item key={item.id} item={item} onSelect={onSelect} />);
});
// Stable callback references — prevent re-renders downstream
const handleSelect = useCallback((id) => {
setSelected(id);
}, []); // no deps: stable forever
// Memoize expensive computations
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Virtualize long lists
import { FixedSizeList } from 'react-window';
<FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => <Row item={items[index]} style={style} />}
</FixedSizeList>
数据库查询性能分析
PostgreSQL EXPLAIN ANALYZE
-- Wrap any query in EXPLAIN (ANALYZE, BUFFERS) to see execution plan
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id;
从输出的底部向上阅读。每个节点都会显示:
actual time=X..Y— 返回第一行所需的启动时间,以及返回所有行所需的总时间rows=N— 实际返回的行数loops=N— 此节点执行的次数
警示信号:
- 大型表上出现
Seq Scan— 缺少索引 - 预估
rows=1000,实际rows=1— 统计信息过时,请运行ANALYZE Hash Join的哈希批次数较多 — 内存压力,请调整work_mem- 大型外部结果集上出现
Nested Loop— 存在笛卡尔积风险
在生产环境中查找慢查询
-- Enable pg_stat_statements extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top 10 slowest queries by total time
SELECT
query,
calls,
total_exec_time / 1000 AS total_seconds,
mean_exec_time AS mean_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
浏览器性能分析
完整的 Chrome Performance 选项卡工作流、Core Web Vitals 测量方法和布局抖动诊断,请参阅 references/browser-profiling.md。
瓶颈分类规则
当用户提供性能分析数据时,使用以下规则对瓶颈进行分类和排序。按优先级处理信号——高优先级信号优先于低优先级信号。
优先级 1:数据库(首先检查——70% 的情况下它是瓶颈)
| 信号 | 分类 |
|---|---|
| 任意查询耗时 >500ms | 严重 — type: database。下一步:对该查询运行 EXPLAIN ANALYZE。检查大型表上的顺序扫描(缺少索引)以及 N+1 模式(使用不同 ID 重复执行相同查询)。 |
| 每个请求有多个耗时 >100ms 的查询 | 高 — type: database。下一步:按端点汇总查询数量。如果每个请求的查询数 >5,检查 N+1 问题或缺少 JOIN。可考虑为每个端点设置查询数量预算。 |
| 每次页面加载的查询数 >20 | 高 — type: database。即使单个查询速度很快,连接开销和往返延迟也会累积。下一步:使用 WHERE id IN (...) 进行批处理,或采用 DataLoader 模式。 |
优先级 2:事件循环(Node.js 特有——最容易被忽视的瓶颈)
| 信号 | 分类 |
|---|---|
| ELU >0.8 | 严重——type: cpu。事件循环已饱和。下一步:运行 clinic flame 或 --prof,找出同步热点路径。常见原因:对大型负载执行 JSON.parse、同步加密、正则表达式回溯。 |
| ELU >0.5 且 p99 延迟较高 | 高——type: cpu。事件循环争用导致尾延迟。下一步:查找不常运行、但运行时会占用事件循环的阻塞操作(大型排序、模板渲染、PDF 生成)。 |
| ELU <0.2 且响应缓慢 | 这不是 CPU 问题。 type: io。下一步:应用正在等待外部资源(数据库、API 调用、文件系统)。使用 clinic bubbleprof 跟踪出站请求,或为外部调用添加计时日志。 |
优先级 3:内存
| 信号 | 分类 |
|---|---|
| 堆持续增长,速率 >10MB/min | 严重——type: memory。内存泄漏会导致进程因内存不足而终止。下一步:间隔 5 分钟拍摄两份堆快照,在 Chrome DevTools 中比较,查看对象数量(保留大小)是否增长。常见嫌疑项:事件监听器不断累积、闭包捕获请求对象、无界缓存。 |
| 堆增长与请求速率成正比(GC 时重置) | 中——type: memory。这不是泄漏,只是分配压力较高。下一步:检查热点路径中是否存在不必要的对象创建(克隆大型对象、通过拼接构建字符串)。减少分配,不要纠结于 GC。 |
堆分析中的 suspects 数组 | 列出每个嫌疑项及其保留大小。如果某个对象保留 >50MB,则标记为高。下一步:沿保留者树追踪,找出对象未被回收的原因。 |
优先级 4:React 渲染(前端)
| 信号 | 分类 |
|---|---|
| 组件渲染时间 >16ms | 高——type: rendering。帧丢失。下一步:检查组件是否在每次父组件渲染时都重新渲染(缺少 React.memo 或属性不稳定)。使用 React DevTools 的“Why did this render?”进行分析。 |
| 每次用户交互触发 >5 次重新渲染 | 中——type: rendering。下一步:检查是否有状态更新触发级联重新渲染。将状态移至更接近其使用位置的地方,或拆分上下文提供程序。 |
| 组件树较大(挂载的组件 >500 个) | 中——type: rendering。下一步:对列表进行虚拟化(react-window),延迟加载屏幕外组件,并检查是否存在不必要的挂载/卸载循环。 |
优先级 5:CPU(非事件循环)
| 信号 | 分类 |
|---|---|
CPU 配置文件中,单个函数占 selfTime >30% | 高——type: cpu。热点函数占主导。下一步:阅读该函数。如果它在你的代码中,就进行优化。如果它在库中,检查你是否不必要地调用它,或传入了异常大的输入。 |
| 火焰图呈现宽而平的轮廓(没有单个热点函数) | 中——type: cpu。积少成多导致性能下降。下一步:查找模式——是否有许多函数执行相似的工作?这通常意味着存在冗余计算(每个请求中多次计算相同的派生值)。 |
输出排名
对所有信号分类后,按以下顺序排列瓶颈列表:
- 严重程度(严重项优先)
- 可操作性(有明确后续步骤的项优先于“进一步调查”这类模糊描述)
- 预估影响——“添加索引可将此查询的耗时从 800ms 降至 5ms”比“这可能有帮助”更有用
始终将 estimatedImpact 写成具体预测:“消除 N+1 查询应可将请求数从 47 降至 3,使端点延迟降低约 200ms”,而不是“应该能提升性能”。
参考资料
references/node-profiling.md— 了解 Node.js 性能分析的详细内容:--inspect 标志、clinic.js 命令、堆快照分析、事件循环监控、流背压诊断references/browser-profiling.md— 了解浏览器性能:Chrome Performance 面板工作流、Lighthouse CI 集成、React Profiler 深入分析、Core Web Vitals 测量、布局抖动模式