我构建了一个小型函数分析器,彻底改变了我调试 JavaScript 的方式

发布: (2025年12月28日 GMT+8 01:55)
14 min read
原文: Dev.to

Source: Dev.to

我写了一个小型函数分析器,它彻底改变了我调试 JavaScript 的方式

在过去的几个月里,我一直在为一个大型前端项目做性能调优。虽然浏览器的 Performance 面板已经非常强大,但我总觉得缺少一种更轻量、即时的方式来了解哪些函数在运行时被调用了多少次、耗时多久。于是,我动手写了一个 tiny‑function‑profiler,它只用了几行代码,却让我对代码的执行路径有了前所未有的洞察。

下面,我会介绍这个分析器的实现细节、使用方法以及它是如何帮助我定位性能瓶颈的。


目录

  1. 为什么需要自定义分析器?
  2. 核心实现
  3. 使用示例
  4. 实际案例:发现隐藏的性能问题
  5. 局限性与改进方向
  6. 完整代码

为什么需要自定义分析器?

  • 即时反馈:浏览器的 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);
}

要点解释

  1. stats 对象 保存每个函数的调用次数 (count) 与累计耗时 (total)。
  2. start(name) 在函数入口记录 performance.now()
  3. end(name) 计算本次调用耗时并更新统计。
  4. report() 使用 console.table 将结果以表格形式输出,便于阅读。
  5. 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 后发现:

FunctionCallsTotal (ms)Avg (ms)
fetchData1250.12250.12
renderList50980.3419.61
updateDOM5001450.782.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 编写——不依赖 lodashmoment,没有冗余。

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

选项

选项类型默认值描述
logbooleanfalse启用控制台日志
maxHistorynumber50要保留的执行次数
colorbooleantrue启用彩色输出

统计对象

属性类型描述
callsnumber调用总次数
errorsnumber错误总次数
lastTimenumber上一次执行时间(毫秒)
historynumber[]最近执行时间数组

history

类型: number[]
描述: 过去执行时间的数组

⚡ 性能

我知道你在想什么:“这会不会让我的代码变慢?”

开销大约是 ~0.05 ms 每次调用。除非你每秒调用函数数百万次,否则你不会注意到。

用于生产环境:

  • 使用 log: false 来禁用控制台输出。
  • 根据你的内存限制调整 maxHistory
  • 历史记录数组会自动受限——不会出现内存泄漏。

🆚 为什么不直接使用 Chrome DevTools?

好问题!Chrome DevTools 很强大,但 function‑trace 提供了 DevTools 开箱即用无法提供的功能。

功能Chrome DevToolsfunction‑trace
在 Node.js 中可用
对数据的编程访问
自定义警报
生产环境监控
零配置
历史数据受限

它们是互补的工具:使用 DevTools 进行深度分析,使用 function‑trace 进行快速调试和生产监控。

🛠️ 最佳实践

  • 开发: 使用 { log: true } 启用日志记录。
  • 生产: 使用 { log: false } 禁用日志记录,但保留统计信息。
  • 高频函数: 降低 maxHistory 以节省内存。
  • 关键路径: 基于 stats.history 构建警报。
  • 调试: 包装可疑函数以查找瓶颈。

🤝 贡献

function‑trace 在 MIT 许可证下开源。欢迎贡献!

🔗 链接

🙏 最后思考

我创建 function‑trace 是为了解决我自己的问题,但我也希望它能帮助到你。

如果你尝试了,欢迎在评论中告诉我!我很想了解:

  • 你是如何使用它的?
  • 你希望看到哪些功能?
  • 它是否帮助你发现了性能问题?

祝调试愉快! 🐛🔍

如果这对你有帮助,考虑关注我以获取更多 JavaScript/TypeScript 内容。我会写关于网页开发、开源以及开发者工具的文章。

你常用的调试技巧是什么?在评论区留下吧! 👇

Back to Blog

相关文章

阅读更多 »

JavaScript

关键趋势 - 运行时创新:Deno 最新发布引入了一个新工具 dx,用于运行 NPM 和 JSR 二进制文件,提升兼容性和开发者工作流……

🚀 停止损害你的 Bundle Size

自动删除 TypeScript 中的 Barrel 文件!封面图片描述:左侧显示一个沉重、纠结的捆绑,右侧显示一个干净、直接的导入……