Bun 3.0 作为基于 JavaScriptCore 引擎的高性能 JavaScript 运行时,启动速度比 Node.js 快 3 倍,包安装速度快 30 倍。但在生产环境中,性能问题往往隐藏在代码深处:某个函数意外消耗大量 CPU 周期,或者内存泄漏导致进程缓慢增长直至崩溃。
本教程将带你系统化掌握 Bun 应用的性能剖析技术,通过火焰图可视化 CPU 热点,使用堆快照定位内存泄漏,并给出可落地的优化方案。
为什么需要性能剖析
开发者常犯的错误是过早优化:花时间优化自以为的"瓶颈",结果发现对整体性能影响微乎其微。火焰图之父 Brendan Gregg 说过:"性能优化的第一原则是测量,不是猜测。"
环境准备
安装 Bun 3.0
确保使用最新版本以获得完整的性能分析 API 支持:
curl -fsSL https://bun.sh/install | bash
bun --version
# 输出:3.0.x
安装 perf(Linux)
perf 是 Linux 内核内置的性能分析工具:
# Ubuntu/Debian
sudo apt-get install linux-tools-common linux-tools-generic
# macOS 使用 Instruments 代替
# 或使用 async-profiler
brew install async-profiler
克隆 FlameGraph 脚本
Brendan Gregg 的 FlameGraph 工具库用于生成 SVG 火焰图:
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph
export PATH=$PWD:$PATH
CPU 性能剖析实战
获取 Bun 进程 PID
首先找到目标 Bun 进程的进程 ID:
# 方法 1:通过进程名查找
ps aux | grep bun
# 方法 2:通过端口查找(假设 Bun 监听 3000 端口)
lsof -ti:3000
# 方法 3:在代码中输出 PID
console.log(`Current PID: ${process.pid}`);
使用 perf 采集 CPU 样本
对目标进程进行 30 秒采样,频率 99Hz:
# -F 99: 采样频率 99Hz(避免与 100Hz 系统任务同步)
# -p <pid>: 目标进程 ID
# -g: 记录调用栈
# -- sleep 30: 采样 30 秒
sudo perf record -F 99 -p 12345 -g -- sleep 30
生成火焰图
将 perf 数据转换为火焰图:
# 1. 导出 perf 数据为文本格式
perf script > out.perf
# 2. 折叠调用栈(合并相同栈帧)
stackcollapse-perf.pl out.perf > out.folded
# 3. 生成 SVG 火焰图
flamegraph.pl out.folded > flamegraph.svg
打开 flamegraph.svg 文件,你会看到类似下图的可视化结果:
解读火焰图
- 横向宽度:表示函数消耗的 CPU 时间占比,越宽越耗时
- 纵向高度:表示调用栈深度,顶层是直接调用者
- 颜色:仅用于区分不同栈帧,无语义含义
Bun 内置性能计时
对于细粒度的函数性能分析,Bun 提供了纳秒级精度的计时器:
基础计时示例
// 基础计时
const startTime = Bun.nanoseconds();
// 待测代码
const result = heavyComputation();
const endTime = Bun.nanoseconds();
const duration = (endTime - startTime) / 1_000_000; // 转换为毫秒
console.log(`Execution time: ${duration.toFixed(2)}ms`);
封装性能分析装饰器
创建可复用的性能分析工具函数:
// lib/profiler.ts
export function profile<T extends (...args: any[]) => any>(
name: string,
fn: T
): T {
return ((...args: Parameters<T>): ReturnType<T> => {
const start = Bun.nanoseconds();
const result = fn(...args);
const duration = Bun.nanoseconds() - start;
// 输出性能日志(生产环境可发送到监控系统)
console.log(`[${name}] ${duration / 1_000_000}ms`);
return result;
}) as T;
}
// 使用示例
const parseData = profile('parseData', (input: string) => {
// 复杂解析逻辑
return JSON.parse(input);
});
批量统计与百分位分析
class PerformanceStats {
private samples: number[] = [];
record(durationMs: number) {
this.samples.push(durationMs);
}
percentile(p: number): number {
const sorted = [...this.samples].sort((a, b) => a - b);
const index = Math.floor((p / 100) * sorted.length);
return sorted[index];
}
report() {
const p50 = this.percentile(50);
const p95 = this.percentile(95);
const p99 = this.percentile(99);
console.log(`P50: ${p50.toFixed(2)}ms`);
console.log(`P95: ${p95.toFixed(2)}ms (95% 请求低于此值)`);
console.log(`P99: ${p99.toFixed(2)}ms (99% 请求低于此值)`);
}
}
// 使用
const stats = new PerformanceStats();
stats.record(measureOneRequest());
stats.report();
内存泄漏诊断
内存泄漏是 Bun/Node.js 应用常见的问题。Bun 支持 V8 堆快照 API,可与 Chrome DevTools 配合使用。
生成堆快照
// 在代码中生成堆快照
import { writeHeapSnapshot } from 'v8';
// 生成快照文件
writeHeapSnapshot('snapshot.heapsnapshot');
// 或在特定操作前后分别生成快照用于对比
writeHeapSnapshot('before.heapsnapshot');
doSomething();
writeHeapSnapshot('after.heapsnapshot');
Chrome DevTools 对比分析
- 打开 Chrome DevTools → Memory 面板
- Load 两份
.heapsnapshot文件 - 选择后加载的快照,切换为 "Comparison" 视图
- 查看对象数量/内存增长的类型
常见泄漏模式:
- Array/Object 持续增长:检查是否有全局缓存无界增长
- Closure 持有大对象:闭包可能意外持有已不需要的大对象
- Event Listener 未清理:移除 DOM 元素前未解绑事件监听器
典型泄漏案例修复
案例 1:无界缓存
// ❌ 错误:缓存无限增长
const cache = new Map();
function getData(id: string) {
if (!cache.has(id)) {
cache.set(id, fetchExpensiveData(id));
}
return cache.get(id);
}
// ✅ 正确:使用 LRU 缓存
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 1000, // 最多 1000 个条目
maxSize: 100_000_000, // 最多 100MB
ttl: 3600_000, // 1 小时过期
});
案例 2:定时器未清理
// ❌ 错误:定时器持续累积
function startMonitoring() {
setInterval(() => {
checkHealth();
}, 5000);
}
// 每次调用都创建新定时器
// ✅ 正确:单例模式 + 清理方法
let monitorInterval: NodeJS.Timeout | null = null;
function startMonitoring() {
if (monitorInterval) return; // 防止重复启动
monitorInterval = setInterval(checkHealth, 5000);
}
function stopMonitoring() {
if (monitorInterval) {
clearInterval(monitorInterval);
monitorInterval = null;
}
}
进阶优化技巧
使用 TypedArray 优化数值计算
// ❌ 普通数组:每个元素是独立对象
const numbers = [1, 2, 3, 4, 5];
// ✅ TypedArray:连续内存块,CPU 缓存友好
const typedNumbers = new Float64Array([1, 2, 3, 4, 5]);
// 性能对比:TypedArray 快 3-5 倍
function sumArray(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0);
}
function sumTypedArray(arr: Float64Array): number {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
避免不必要的对象分配
// ❌ 每次调用都创建新对象
function processData(data: string) {
return {
original: data,
upper: data.toUpperCase(),
length: data.length,
};
}
// ✅ 复用对象(适用于高频调用场景)
const resultCache = {
original: '',
upper: '',
length: 0,
};
function processDataReuse(data: string) {
resultCache.original = data;
resultCache.upper = data.toUpperCase();
resultCache.length = data.length;
return resultCache;
}
懒加载模块减少启动时间
// ❌ 启动时加载所有模块
import { heavyModule1 } from './heavy1';
import { heavyModule2 } from './heavy2';
import { heavyModule3 } from './heavy3';
// ✅ 按需加载
async function loadModule(name: string) {
switch (name) {
case 'heavy1':
return await import('./heavy1');
case 'heavy2':
return await import('./heavy2');
case 'heavy3':
return await import('./heavy3');
}
}
// 或使用 top-level await 懒加载
let heavyModule: typeof import('./heavy1') | null = null;
async function getHeavyModule() {
heavyModule ??= await import('./heavy1');
return heavyModule;
}
常见问题 FAQ
火焰图为什么有很多窄条?
窄条表示采样次数少的函数,可能是偶发调用。聚焦宽度超过 10% 的热点,优化它们收益最大。
perf 采样会影响生产环境性能吗?
99Hz 采样对性能影响通常在 1-3%,但建议先在预发环境测试。生产环境可使用 async-profiler 的事件驱动模式降低开销。
如何定位偶发性卡顿?
使用 Bun 的 --watch 模式配合连续采样,或使用 async-profiler 的 wall-clock 模式捕获真实时间的阻塞。
堆快照太大怎么办?
尝试只捕获部分堆(使用 HeapSnapshotWorker),或使用 Chrome DevTools 的 "Allocation Sampling" 模式减少内存占用。
Bun 3.0 有什么性能分析新特性?
Bun 3.0 改进了 JavaScriptCore 的性能计数器集成,支持更精确的 CPU 周期统计。同时优化了 Bun.nanoseconds() 的精度。
总结
- ✓ 使用 perf + FlameGraph 可视化 CPU 热点,找到真正的性能瓶颈
- ✓ 使用 Bun.nanoseconds() 进行细粒度函数计时,P95/P99 评估延迟分布
- ✓ 使用 V8 堆快照 + Chrome DevTools 对比分析内存泄漏
- ✓ 使用 LRU 缓存、TypedArray、懒加载 等技巧优化性能
- ✓ 测量优先于猜测,用数据驱动优化决策