Node.js Performance Profiling
When NOT to Use This Skill
- Java/JVM profiling - Use the
java-profiling skill for JFR, jcmd, and GC tuning - Python profiling - Use the
python-profiling skill for cProfile and memory_profiler - Frontend performance - Use browser DevTools for client-side profiling
- Database query optimization - Use database-specific profiling tools
- Network performance - Use tools like curl, ab, or specialized load testers
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: nodejs for comprehensive profiling guides, V8 flags, and optimization techniques.
V8 CPU Profiling
Command Line Profiling
# CPU profile (generates .cpuprofile)
node --cpu-prof --cpu-prof-dir=./profiles app.js
# V8 profile (generates .log)
node --prof app.js
node --prof-process isolate-*.log > processed.txt
# Heap snapshot on signal
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>
Programmatic Profiling
import { Session } from 'inspector';
import { writeFileSync } from 'fs';
const session = new Session();
session.connect();
// Start CPU profiling
session.post('Profiler.enable');
session.post('Profiler.start');
// Your code here...
// Stop and get profile
session.post('Profiler.stop', (err, { profile }) => {
writeFileSync('profile.cpuprofile', JSON.stringify(profile));
});
Memory Analysis
Heap Statistics
import v8 from 'v8';
const heapStats = v8.getHeapStatistics();
console.log({
heapUsed: heapStats.used_heap_size,
heapTotal: heapStats.total_heap_size,
heapLimit: heapStats.heap_size_limit,
external: heapStats.external_memory,
});
// Detailed heap space info
const heapSpaces = v8.getHeapSpaceStatistics();
heapSpaces.forEach(space => {
console.log(`${space.space_name}: ${space.space_used_size}`);
});
Memory Tracking
import { performance, PerformanceObserver } from 'perf_hooks';
// Track memory at intervals
const memoryTracker = setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: usage.rss, // Resident Set Size
heapTotal: usage.heapTotal,
heapUsed: usage.heapUsed,
external: usage.external,
arrayBuffers: usage.arrayBuffers,
});
}, 1000);
High-Resolution Timing
perf_hooks API
import { performance, PerformanceObserver } from 'perf_hooks';
// Mark start/end
performance.mark('operation-start');
await someOperation();
performance.mark('operation-end');
// Measure duration
performance.measure('operation', 'operation-start', 'operation-end');
// Observer for async measurements
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
obs.observe({ entryTypes: ['measure', 'function'] });
// Cleanup
performance.clearMarks();
performance.clearMeasures();
Async Context Tracking
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
const storage = new AsyncLocalStorage<{ requestId: string }>();
// Track request timing across async operations
function trackRequest(requestId: string) {
storage.run({ requestId }, async () => {
const start = performance.now();
await handleRequest();
const duration = performance.now() - start;
console.log(`Request ${requestId}: ${duration}ms`);
});
}
Common Bottleneck Patterns
CPU-Bound Issues
// ❌ Bad: Blocking the event loop
function processLargeArray(arr: number[]): number {
return arr.reduce((sum, n) => sum + expensiveComputation(n), 0);
}
// ✅ Good: Use worker threads
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
if (isMainThread) {
const worker = new Worker(__filename, { workerData: largeArray });
worker.on('message', (result) => console.log(result));
} else {
const result = workerData.reduce((sum, n) => sum + expensiveComputation(n), 0);
parentPort?.postMessage(result);
}
I/O-Bound Issues
// ❌ Bad: Sequential I/O
for (const file of files) {
await fs.readFile(file); // One at a time
}
// ✅ Good: Parallel I/O with concurrency limit
import pLimit from 'p-limit';
const limit = pLimit(10);
await Promise.all(
files.map(file => limit(() => fs.readFile(file)))
);
Memory Leaks
// ❌ Bad: Unbounded cache
const cache = new Map();
function getUser(id: string) {
if (!cache.has(id)) {
cache.set(id, fetchUser(id)); // Never cleaned up
}
return cache.get(id);
}
// ✅ Good: LRU cache with max size
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, User>({
max: 1000,
ttl: 1000 * 60 * 5, // 5 minutes
});
// ❌ Bad: Event listener leak
element.addEventListener('click', handler); // Never removed
// ✅ Good: Cleanup listeners
const abortController = new AbortController();
element.addEventListener('click', handler, { signal: abortController.signal });
// Later: abortController.abort();
GC Pressure
// ❌ Bad: Creating many temporary objects
function process(items: Item[]) {
return items.map(item => ({
...item,
computed: compute(item),
}));
}
// ✅ Good: Mutate in place when safe
function process(items: Item[]) {
for (const item of items) {
item.computed = compute(item);
}
return items;
}
// ✅ Good: Object pooling
class ObjectPool<T> {
private pool: T[] = [];
acquire(): T {
return this.pool.pop() || this.create();
}
release(obj: T) {
this.reset(obj);
this.pool.push(obj);
}
}
Optimization Techniques
Buffer Optimization
// ❌ Bad: Many small allocations
const chunks: Buffer[] = [];
for (const data of stream) {
chunks.push(Buffer.from(data));
}
const result = Buffer.concat(chunks);
// ✅ Good: Pre-allocate when size known
const buffer = Buffer.allocUnsafe(totalSize); // Faster, uninitialized
let offset = 0;
for (const data of stream) {
offset += data.copy(buffer, offset);
}
Stream Processing
// ❌ Bad: Loading entire file in memory
const data = await fs.readFile('large-file.json');
const parsed = JSON.parse(data);
// ✅ Good: Stream processing
import { createReadStream } from 'fs';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = createReadStream('large-file.json')
.pipe(parser())
.pipe(streamArray());
for await (const { value } of pipeline) {
await processItem(value);
}
V8 Optimization Hints
// Force V8 to optimize a function
function criticalFunction(x: number): number {
// Called many times with same types
return x * 2;
}
// Warm up
for (let i = 0; i < 10000; i++) criticalFunction(i);
// Avoid deoptimization patterns:
// - Don't change object shapes after creation
// - Don't use delete on object properties
// - Don't use arguments object, use rest parameters
// - Don't use with statement
// - Keep function polymorphism low
Profiling Checklist
| Check | Tool | Command |
|---|
| CPU hotspots | CPU profile | node --cpu-prof app.js |
| Memory usage | Heap stats | v8.getHeapStatistics() |
| Memory leaks | Heap snapshot | --heapsnapshot-signal |
| Event loop lag | perf_hooks | monitorEventLoopDelay() |
| Async operations | Async hooks | async_hooks module |
| Function timing | perf_hooks | performance.measure() |
GC Tuning
# Increase heap size
node --max-old-space-size=4096 app.js
# GC logging
node --trace-gc app.js
# Expose GC for manual control
node --expose-gc app.js
# In code: global.gc();
Anti-Patterns
| Anti-Pattern | Why It's Wrong | Correct Approach |
|---|
Using setImmediate() for CPU work | Blocks event loop | Use worker threads for CPU-intensive tasks |
| Synchronous file operations | Blocks entire process | Use async fs.promises API |
| Large synchronous JSON parsing | Freezes event loop | Stream large JSON or use worker threads |
| Callback hell | Hard to profile, error-prone | Use async/await for cleaner async code |
| Not using connection pooling | Creates too many connections | Use connection pools (pg, mysql2) |
console.log() in production | Slow, blocks event loop | Use structured logging (pino, winston) |
| Loading entire file into memory | Memory exhaustion | Use streams for large files |
| Manual cache without TTL/limits | Memory leaks | Use LRU cache with size/time limits |
| Not monitoring event loop lag | Undetected performance degradation | Use perf_hooks.monitorEventLoopDelay() |
delete on object properties | Deoptimizes objects | Set to undefined or use Map |
Quick Troubleshooting
| Issue | Diagnosis | Solution |
|---|
| High CPU usage | Tight loops, inefficient algorithms | Profile with --cpu-prof, optimize hot paths |
| Memory growing continuously | Memory leak (unbounded cache, listeners) | Take heap snapshots, compare over time |
| Event loop lag | Long synchronous operations | Use worker threads or break into async chunks |
| GC pauses causing latency spikes | Heap too large or fragmented | Reduce heap size, optimize object creation |
| Slow startup time | Too many synchronous requires | Lazy load modules, use dynamic imports |
FATAL ERROR: CALL_AND_RETRY_LAST | Out of memory | Increase --max-old-space-size or fix memory leak |
| High memory usage | Large buffers, string operations | Use streams, avoid string concatenation |
| Unhandled promise rejections | Async errors not caught | Add .catch() or use try/catch with async/await |
| Function not optimized by V8 | Contains deopt triggers | Check with --trace-deopt, avoid problematic patterns |
| Slow JSON operations | Large payloads | Stream JSON or use faster parsers (simdjson) |