Difficulty: Advanced
How do you identify and fix performance bottlenecks in a Node.js application? Describe your profiling workflow.
Performance profiling in Node.js involves identifying CPU bottlenecks, memory leaks, slow database queries, and event loop blocking. The process starts with monitoring production metrics, reproducing the issue, profiling to find the root cause, and verifying the fix.
Node.js provides built-in tools: --inspect flag for Chrome DevTools debugging, --prof for V8 profiler output, and process.memoryUsage() for memory tracking. Third-party tools like clinic.js provide flame graphs, event loop analysis, and heap profiling.
Memory leaks are the most common production issue. They occur when objects accumulate in memory and are never garbage collected. Common causes: growing arrays/maps, unclosed event listeners, closures holding references, and global caches without eviction. Heap snapshots taken at intervals can reveal growing object counts.
For CPU profiling, flame graphs show which functions consume the most time. Look for wide bars (slow functions) and tall stacks (deep call chains). Event loop delay monitoring reveals if synchronous work is blocking the loop. A delay above 100ms typically indicates a problem.
// Monitor memory usage
function logMemory() {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(1)}MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(1)}MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(1)}MB`,
external: `${(usage.external / 1024 / 1024).toFixed(1)}MB`,
});
}
setInterval(logMemory, 10000);
// Common memory leak: growing array
const cache = []; // LEAK: grows forever!
app.get('/api/data', (req, res) => {
const data = fetchData();
cache.push(data); // Never cleaned up
res.json(data);
});
// Fix: use LRU cache with max size
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 }); // 1000 items, 5min TTL
// Common leak: event listeners
const EventEmitter = require('events');
const emitter = new EventEmitter();
app.get('/api/stream', (req, res) => {
// LEAK: new listener added every request, never removed
emitter.on('data', (d) => res.write(d));
// FIX: remove listener when request ends
const handler = (d) => res.write(d);
emitter.on('data', handler);
req.on('close', () => emitter.removeListener('data', handler));
});
Growing heapUsed over time indicates a memory leak. LRU caches auto-evict old entries. Always remove event listeners when they are no longer needed.
// Event loop delay monitoring
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log({
min: `${(histogram.min / 1e6).toFixed(1)}ms`,
max: `${(histogram.max / 1e6).toFixed(1)}ms`,
mean: `${(histogram.mean / 1e6).toFixed(1)}ms`,
p99: `${(histogram.percentile(99) / 1e6).toFixed(1)}ms`,
});
histogram.reset();
}, 5000);
// CPU profiling with --inspect
// node --inspect src/index.js
// Open chrome://inspect in Chrome
// -> Performance tab -> Record -> Run workload -> Stop
// Programmatic CPU profiling
const v8Profiler = require('v8-profiler-next');
app.get('/debug/profile', (req, res) => {
const title = `profile-${Date.now()}`;
v8Profiler.startProfiling(title, true);
setTimeout(() => {
const profile = v8Profiler.stopProfiling(title);
profile.export((error, result) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename=${title}.cpuprofile`);
res.send(result);
profile.delete();
});
}, 10000); // Profile for 10 seconds
});
Event loop delay shows if synchronous work is blocking. Values above 100ms indicate problems. CPU profiles can be loaded into Chrome DevTools for analysis.
// Simple benchmarking with console.time
console.time('db-query');
const users = await prisma.user.findMany({ take: 1000 });
console.timeEnd('db-query'); // db-query: 45.2ms
// Performance hooks for detailed measurement
const { performance, PerformanceObserver } = require('perf_hooks');
// Measure function execution time
performance.mark('start');
await heavyOperation();
performance.mark('end');
performance.measure('heavyOperation', 'start', 'end');
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`);
}
});
observer.observe({ entryTypes: ['measure'] });
// Express middleware to track response times
app.use((req, res, next) => {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
if (duration > 1000) {
console.warn(`SLOW: ${req.method} ${req.path} took ${duration.toFixed(0)}ms`);
}
});
next();
});
// Load testing with autocannon (CLI)
// npx autocannon -c 100 -d 30 http://localhost:3000/api/users
// -c 100: 100 concurrent connections
// -d 30: run for 30 seconds
// Output: requests/sec, latency percentiles, throughput
Use console.time for quick checks, perf_hooks for detailed measurement, and autocannon for load testing. Track slow routes in production.
Profiling, Memory Leaks, CPU Profiling, Heap Snapshot, Benchmarking