我构建了一个小型函数分析器,彻底改变了我调试 JavaScript 的方式
Source: Dev.to
我写了一个小型函数分析器,它彻底改变了我调试 JavaScript 的方式
在过去的几个月里,我一直在为一个大型前端项目做性能调优。虽然浏览器的 Performance 面板已经非常强大,但我总觉得缺少一种更轻量、即时的方式来了解哪些函数在运行时被调用了多少次、耗时多久。于是,我动手写了一个 tiny‑function‑profiler,它只用了几行代码,却让我对代码的执行路径有了前所未有的洞察。
下面,我会介绍这个分析器的实现细节、使用方法以及它是如何帮助我定位性能瓶颈的。
目录
为什么需要自定义分析器?
- 即时反馈:浏览器的 Performance 面板需要手动打开、记录、停止,且结果往往是一次性的。我的分析器可以在代码运行时实时打印统计信息。
- 最小侵入:只需要在感兴趣的函数前后加上一行
profile(fnName),不必改动函数内部逻辑。 - 可定制:可以根据项目需求自行决定统计粒度(调用次数、累计耗时、平均耗时等)。
核心实现
// profiler.js
const profiler = (() => {
const stats = {};
function start(name) {
if (!stats[name]) stats[name] = { count: 0, total: 0 };
stats[name].start = performance.now();
}
function end(name) {
const end = performance.now();
const entry = stats[name];
entry.count++;
entry.total += end - entry.start;
}
function report() {
console.table(
Object.entries(stats).map(([name, { count, total }]) => ({
Function: name,
Calls: count,
'Total (ms)': total.toFixed(2),
'Avg (ms)': (total / count).toFixed(2),
}))
);
}
// 让外部可以直接调用 profile('fnName')
return { start, end, report };
})();
// 简化调用的包装函数
function profile(name) {
profiler.start(name);
return () => profiler.end(name);
}
要点解释:
stats对象 保存每个函数的调用次数 (count) 与累计耗时 (total)。start(name)在函数入口记录performance.now()。end(name)计算本次调用耗时并更新统计。report()使用console.table将结果以表格形式输出,便于阅读。profile(name)返回一个闭包,调用该闭包即可自动执行end(name),这让包装函数的写法非常简洁。
使用示例
import { profile } from './profiler.js';
// 假设我们有一个耗时的函数
function heavyComputation(arr) {
// ... 复杂逻辑 ...
return arr.reduce((a, b) => a + b, 0);
}
// 包装调用
function wrappedHeavyComputation(arr) {
const stop = profile('heavyComputation');
const result = heavyComputation(arr);
stop(); // 记录结束时间
return result;
}
// 在代码的其他地方多次调用
for (let i = 0; i < 100; i++) {
wrappedHeavyComputation(Array.from({ length: 1000 }, (_, i) => i));
}
// 最后输出报告
profiler.report();
运行上述代码后,控制台会显示类似下面的表格:
┌───────────────┬───────┬─────────────┬───────────┐
│ Function │ Calls │ Total (ms) │ Avg (ms) │
├───────────────┼───────┼─────────────┼───────────┤
│ heavyComputation │ 100 │ 123.45 │ 1.23 │
└───────────────┴───────┴─────────────┴───────────┘
实际案例:发现隐藏的性能问题
在我的项目中,有一个名为 renderList 的函数负责把大量数据渲染到页面上。起初,我以为性能瓶颈在网络请求上,但使用 tiny‑function‑profiler 后发现:
| Function | Calls | Total (ms) | Avg (ms) |
|---|---|---|---|
| fetchData | 1 | 250.12 | 250.12 |
| renderList | 50 | 980.34 | 19.61 |
| updateDOM | 500 | 1450.78 | 2.90 |
updateDOM 的调用次数远超预期,而且累计耗时占比最高。进一步检查后,我发现每次渲染都会触发一次不必要的 reflow,导致页面卡顿。通过把 updateDOM 合并为批量更新,整体渲染时间从 ~1.2 s 降到了 ≈ 0.4 s。
局限性与改进方向
| 限制 | 说明 | 可能的改进 |
|---|---|---|
| 只能在浏览器环境使用 | 依赖 performance.now() | 引入 process.hrtime 以支持 Node.js |
| 手动包装函数略显繁琐 | 需要在每个目标函数前后添加 profile 调用 | 使用 装饰器(ES2022+)或 代理 自动拦截函数调用 |
| 统计粒度固定 | 只能统计调用次数和累计耗时 | 增加 最大耗时、最小耗时、标准差 等统计项 |
| 结果只能打印到控制台 | 不便于持久化或可视化 | 导出 JSON,配合图表库(如 Chart.js)生成可交互报告 |
完整代码
// profiler.js
const profiler = (() => {
const stats = {};
function start(name) {
if (!stats[name]) stats[name] = { count: 0, total: 0 };
stats[name].start = performance.now();
}
function end(name) {
const end = performance.now();
const entry = stats[name];
entry.count++;
entry.total += end - entry.start;
}
function report() {
console.table(
Object.entries(stats).map(([name, { count, total }]) => ({
Function: name,
Calls: count,
'Total (ms)': total.toFixed(2),
'Avg (ms)': (total / count).toFixed(2),
}))
);
}
// 让外部可以直接调用 profile('fnName')
return { start, end, report };
})();
function profile(name) {
profiler.start(name);
return () => profiler.end(name);
}
// 示例用法
function heavyComputation(arr) {
// 模拟耗时操作
let sum = 0;
for (let i = 0; i < arr.length; i++) sum += arr[i];
return sum;
}
function wrappedHeavyComputation(arr) {
const stop = profile('heavyComputation');
const result = heavyComputation(arr);
stop();
return result;
}
// 多次调用以产生统计数据
for (let i = 0; i < 100; i++) {
wrappedHeavyComputation(Array.from({ length: 1000 }, (_, i) => i));
}
// 输出报告
profiler.report();
小结
- tiny‑function‑profiler 只用了不到 30 行代码,却提供了实时、可定制的函数级性能统计。
- 它帮助我快速定位了项目中最耗时的函数,并通过针对性优化显著提升了用户体验。
- 由于实现极其轻量,你可以把它直接拷贝进自己的项目,或根据需求自行扩展。
如果你也在寻找一种快速、低侵入的调试手段,不妨试试上面的实现,或在此基础上加入自己的改进。祝你调试顺利!
你是否曾经想过:
- “这个函数为什么这么慢?”
- “它被调用了多少次?”
- “我的代码到底在哪里出错?”
我每天都在问自己这些问题 每一天。于是我构建了一个工具来回答它们。
介绍 function‑trace
一个体积小、零依赖的 JavaScript 和 TypeScript 函数性能分析器。
🤯 问题
上个月我在调试一个生产环境的问题。一个 API 接口偶尔会变慢,但我无法确定是哪个函数导致的。
我的调试过程如下:
const start = performance.now();
const result = await someFunction();
const end = performance.now();
console.log(`someFunction took ${end - start}ms`);
我把这段代码 复制粘贴 到所有地方。它是:
- ❌ 丑陋
- ❌ 耗时
- ❌ 容易忘记删除
- ❌ 没有历史数据
必须有更好的办法。
💡 解决方案
我构建了 function‑trace —— 一个能够自动完成所有这些操作的包装器:
import { trace } from 'function-trace';
const fetchUser = trace(
async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
},
{ log: true }
);
await fetchUser(1);
// [function-trace] fetchUser executed in 142.35ms
一行代码。 就是这么简单。无需设置。无需配置。只需包装后直接使用。
✨ 让它与众不同的特性
1. 兼容所有情况
| 类型 | 支持 |
|---|---|
| 同步函数 | ✅ |
| 异步函数 | ✅ |
| 箭头函数 | ✅ |
| 类方法 | ✅ |
// Sync
const add = trace((a, b) => a + b, { log: true });
// Async
const fetchData = trace(async () => {
return await fetch('/api/data');
}, { log: true });
2. 内置统计
每个被追踪的函数都会拥有一个 .stats 属性:
const myFunction = trace(someFunction, { log: true });
myFunction();
myFunction();
myFunction();
console.log(myFunction.stats);
// {
// calls: 3,
// errors: 0,
// lastTime: 0.02,
// history: [0.02, 0.01, 0.02]
// }
- calls – 调用总次数
- errors – 抛出错误的次数
- lastTime – 最近一次执行时间(毫秒)
- history – 过去执行时间的数组
3. 性能警报
利用统计信息自行构建监控:
const criticalOperation = trace(async () => {
// Some critical operation
}, { log: true, maxHistory: 100 });
async function executeWithMonitoring() {
await criticalOperation();
const avgTime =
criticalOperation.stats.history.reduce((a, b) => a + b, 0) /
criticalOperation.stats.history.length;
if (avgTime > 1000) {
console.error('⚠️ SLA 阈值已超出!');
// 将警报发送到你的监控服务
}
}
4. 零依赖
整个包纯 TypeScript 编写——不依赖 lodash、moment,没有冗余。
5. 类型安全
完整的 TypeScript 支持,具备正确的类型推断:
const multiply = trace(
(a: number, b: number): number => a * b,
{ log: true }
);
const result = multiply(3, 4); // result 的类型为 number
🚀 实际案例
数据库查询监控
import { trace } from 'function-trace';
const getUserById = trace(
async (userId) => {
return await db.users.findById(userId);
},
{ log: true, maxHistory: 100 }
);
// Later, check if queries are getting slow
const avgQueryTime =
getUserById.stats.history.reduce((a, b) => a + b, 0) /
getUserById.stats.history.length;
if (avgQueryTime > 100) {
console.warn('⚠️ Database queries are slow. Consider adding an index.');
}
API 端点分析
const apiCall = trace(
async (endpoint) => {
const response = await fetch(endpoint);
return response.json();
},
{ log: true }
);
// After some usage
console.log(`Total API calls: ${apiCall.stats.calls}`);
console.log(`Failed calls: ${apiCall.stats.errors}`);
console.log(`Last response time: ${apiCall.stats.lastTime}ms`);
查找性能瓶颈
const step1 = trace(processData, { log: true });
const step2 = trace(transformData, { log: true });
const step3 = trace(saveData, { log: true });
await step1(data);
await step2(data);
await step3(data);
// Console output shows exactly where time is spent:
// [function-trace] processData executed in 5.23ms
// [function-trace] transformData executed in 234.56ms
选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
log | boolean | false | 启用控制台日志 |
maxHistory | number | 50 | 要保留的执行次数 |
color | boolean | true | 启用彩色输出 |
统计对象
| 属性 | 类型 | 描述 |
|---|---|---|
calls | number | 调用总次数 |
errors | number | 错误总次数 |
lastTime | number | 上一次执行时间(毫秒) |
history | number[] | 最近执行时间数组 |
history
类型: number[]
描述: 过去执行时间的数组
⚡ 性能
我知道你在想什么:“这会不会让我的代码变慢?”
开销大约是 ~0.05 ms 每次调用。除非你每秒调用函数数百万次,否则你不会注意到。
用于生产环境:
- 使用
log: false来禁用控制台输出。 - 根据你的内存限制调整
maxHistory。 - 历史记录数组会自动受限——不会出现内存泄漏。
🆚 为什么不直接使用 Chrome DevTools?
好问题!Chrome DevTools 很强大,但 function‑trace 提供了 DevTools 开箱即用无法提供的功能。
| 功能 | Chrome DevTools | function‑trace |
|---|---|---|
| 在 Node.js 中可用 | ❌ | ✅ |
| 对数据的编程访问 | ❌ | ✅ |
| 自定义警报 | ❌ | ✅ |
| 生产环境监控 | ❌ | ✅ |
| 零配置 | ❌ | ✅ |
| 历史数据 | 受限 | ✅ |
它们是互补的工具:使用 DevTools 进行深度分析,使用 function‑trace 进行快速调试和生产监控。
🛠️ 最佳实践
- 开发: 使用
{ log: true }启用日志记录。 - 生产: 使用
{ log: false }禁用日志记录,但保留统计信息。 - 高频函数: 降低
maxHistory以节省内存。 - 关键路径: 基于
stats.history构建警报。 - 调试: 包装可疑函数以查找瓶颈。
🤝 贡献
function‑trace 在 MIT 许可证下开源。欢迎贡献!
🔗 链接
🙏 最后思考
我创建 function‑trace 是为了解决我自己的问题,但我也希望它能帮助到你。
如果你尝试了,欢迎在评论中告诉我!我很想了解:
- 你是如何使用它的?
- 你希望看到哪些功能?
- 它是否帮助你发现了性能问题?
祝调试愉快! 🐛🔍
如果这对你有帮助,考虑关注我以获取更多 JavaScript/TypeScript 内容。我会写关于网页开发、开源以及开发者工具的文章。
你常用的调试技巧是什么?在评论区留下吧! 👇