DEV Community

Cover image for I Built a Tiny Function Profiler That Changed How I Debug JavaScript
Masum Billah
Masum Billah

Posted on

I Built a Tiny Function Profiler That Changed How I Debug JavaScript

Have you ever wondered:

  • "Why is this function so slow?"
  • "How many times is this being called?"
  • "Where exactly is my code failing?"

I was asking myself these questions every single day. So I built a tool to answer them.

Introducing function-trace — a tiny, zero-dependency function profiler for JavaScript and TypeScript.


🤯 The Problem

Last month, I was debugging a production issue. An API endpoint was randomly slow, but I couldn't figure out which function was the culprit.

My debugging process looked like this:

const start = performance.now();
const result = await someFunction();
const end = performance.now();
console.log(`someFunction took ${end - start}ms`);
Enter fullscreen mode Exit fullscreen mode

I was copy-pasting this everywhere. It was:

  • ❌ Ugly
  • ❌ Time-consuming
  • ❌ Easy to forget to remove
  • ❌ No historical data

There had to be a better way.


💡 The Solution

I built function-trace — a wrapper that does all of this automatically:

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
Enter fullscreen mode Exit fullscreen mode

One line. That's it. No setup. No configuration. Just wrap and go.


✨ Features That Make It Special

1. Works with Everything

Sync functions? ✅
Async functions? ✅
Arrow functions? ✅
Class methods? ✅

// Sync
const add = trace((a, b) => a + b, { log: true });

// Async
const fetchData = trace(async () => {
    return await fetch('/api/data');
}, { log: true });
Enter fullscreen mode Exit fullscreen mode

2. Built-in Statistics

Every traced function comes with a .stats property:

const myFunction = trace(someFunction, { log: true });

// Call it a few times
myFunction();
myFunction();
myFunction();

console.log(myFunction.stats);
// {
//   calls: 3,
//   errors: 0,
//   lastTime: 0.02,
//   history: [0.02, 0.01, 0.02]
// }
Enter fullscreen mode Exit fullscreen mode

You get:

  • calls — Total number of invocations
  • errors — How many times it threw an error
  • lastTime — Most recent execution time
  • history — Array of past execution times

3. Performance Alerts

Build your own monitoring with the stats:

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 threshold exceeded!');
        // Send alert to your monitoring service
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Zero Dependencies

The entire package is pure TypeScript. No lodash. No moment. No bloat.

Just clean, efficient code.

5. Type-Safe

Full TypeScript support with proper type inference:

const multiply = trace(
    (a: number, b: number): number => a * b,
    { log: true }
);

const result = multiply(3, 4); // result is typed as number
Enter fullscreen mode Exit fullscreen mode

🚀 Real-World Use Cases

Database Query Monitoring

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.');
}
Enter fullscreen mode Exit fullscreen mode

API Endpoint Profiling

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`);
Enter fullscreen mode Exit fullscreen mode

Finding Performance Bottlenecks

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  <-- Found it!
// [function-trace] saveData executed in 12.34ms
Enter fullscreen mode Exit fullscreen mode

📦 Installation

npm install function-trace
Enter fullscreen mode Exit fullscreen mode

or

yarn add function-trace
Enter fullscreen mode Exit fullscreen mode

or

pnpm add function-trace
Enter fullscreen mode Exit fullscreen mode

🎯 API Reference

trace<F>(fn: F, options?: TraceOptions): F & { stats: TraceStats }
Enter fullscreen mode Exit fullscreen mode

Options

Option Type Default Description
log boolean false Enable console logging
maxHistory number 50 Number of execution times to keep
color boolean true Enable colored output

Stats Object

Property Type Description
calls number Total number of calls
errors number Total number of errors
lastTime number Last execution time (ms)
history number[] Array of past execution times

⚡ Performance

I know what you're thinking: "Won't this slow down my code?"

The overhead is approximately ~0.05ms per call. Unless you're calling a function millions of times per second, you won't notice it.

For production:

  • Use log: false to disable console output
  • Adjust maxHistory based on your memory constraints
  • The history array is automatically bounded — no memory leaks

🆚 Why Not Just Use Chrome DevTools?

Great question! Chrome DevTools is amazing, but:

Feature Chrome DevTools function-trace
Works in Node.js
Programmatic access to data
Custom alerting
Production monitoring
Zero setup
Historical data Limited

They're complementary tools. Use DevTools for deep profiling, use function-trace for quick debugging and production monitoring.


🛠️ Best Practices

  1. Development: Enable logging with { log: true }
  2. Production: Disable logging with { log: false } but keep stats
  3. High-frequency functions: Lower maxHistory to save memory
  4. Critical paths: Build alerts based on stats.history
  5. Debugging: Wrap suspect functions to find bottlenecks

🤝 Contributing

This is open source under the MIT license. Contributions are welcome!


🔗 Links


🙏 Final Thoughts

I built function-trace to solve my own problems, but I hope it helps you too.

If you try it out, let me know in the comments! I'd love to hear:

  • How are you using it?
  • What features would you like to see?
  • Did it help you find a performance issue?

Happy debugging! 🐛🔍


If this helped you, consider following me for more JavaScript/TypeScript content. I write about web development, open source, and developer tools.


What's your go-to debugging technique? Drop it in the comments! 👇

Top comments (0)