When an application is slow, most developers reach for console.time(), add some log statements, and guess at the bottleneck. This approach finds obvious problems but misses subtle ones — and it is never systematic.
Flame graphs are a visualization technique invented by Brendan Gregg (Netflix) that shows exactly where CPU time is spent in a running application, from every function call down to the system level. They transform "this is slow" into "this specific function on this call stack consumes 43% of CPU time."
This post teaches you to read flame graphs, generate them for Node.js and Next.js applications, and interpret the patterns that indicate common performance problems.
How to Read a Flame Graph
A flame graph looks like a series of stacked colored bars arranged horizontally:
FLAME GRAPH ANATOMY:
╔══════════════════════════════╗
║ handleRequest (100%) ║ ← Root (widest = most time)
╠══════════════════════╦═══════╣
║ processQuery (68%) ║auth ║ ← Child functions
╠══════════════════╦═══╩═══════╣
║ dbQuery (55%) ║ validate ║ ← Deeper call stack
╠══════════════╦═══╩═══════════╣
║ pgExecute(40%║ serialize ║ ← Bottom = leaf functions
╚══════════════╩═══════════════╝
Time →The rules:
- X-axis = time spent (wider = more CPU time).
- Y-axis = call stack depth (bottom = leaf functions called most recently).
- Width = proportion of total CPU time for that function on that call path.
- Color = usually arbitrary (indicates different modules or call types).
The key insight: look for wide boxes near the top of the graph — these are functions that consume significant CPU time. Look for wide boxes near the bottom — these are leaf functions (doing actual work) that consume significant time.
Pattern 1: The "Sunburn" (Hot Spot)
╔═════════════════════════════════╗
║ handleCheckout (100%) ║
╠═══════════════════════╦═════════╣
║ processPayment (78%) ║ ║
╠═══════════════════════╣ ║
║ jsonStringify (72%) ║ ║ ← 72% of ALL time in one function!
╠═══════════════════════╣ ║
║ JSON.stringify (72%) ║ ║
╚═══════════════════════╩═════════╝When you see a very wide box that spans nearly the full width, you've found your bottleneck. In this example, JSON.stringify is consuming 72% of checkout processing time — almost certainly serializing a large object unnecessarily.
Fix: Reduce the object being serialized, use superjson, or cache the serialized output.
Pattern 2: The "Tower" (Deep Recursion or Call Chain)
╔════════════════════════╗
║ render (100%) ║
╠════════════════════════╣
║ renderComponent (98%) ║
╠════════════════════════╣
║ renderChild (95%) ║
╠════════════════════════╣
║ renderGrandchild (90%)║ ← Deep call stack, each consuming nearly 100%
╠════════════════════════╣
║ renderLeaf (85%) ║
╚════════════════════════╝When you see many layers with each nearly as wide as the parent, the time is being consumed by many tiny recursive or chained calls rather than one bottleneck. The fix is to reduce call chain depth, memoize intermediate results, or batch operations.
Pattern 3: The "Plateau" (Parallel Bottleneck)
╔═══════════════════════════════════════╗
║ handleDashboard (100%) ║
╠═══════════╦══════════╦════════════════╣
║ fetchUsers║fetchOrders║ fetchProducts ║ ← Multiple wide boxes at same level
╠═══════════╣══════════╣════════════════╣
║ sql (33%)║ sql (33%)║ sql (33%) ║ ← Three sequential SQL queries
╚═══════════╩══════════╩════════════════╝Three separate database queries running sequentially, each consuming one-third of the total time. The fix is Promise.all():
// Before: 3 sequential queries (~300ms)
const users = await fetchUsers();
const orders = await fetchOrders();
const products = await fetchProducts();
// After: 3 parallel queries (~100ms)
const [users, orders, products] = await Promise.all([
fetchUsers(),
fetchOrders(),
fetchProducts(),
]);Generating Flame Graphs for Node.js
Method 1: Node.js Built-in Profiler
# Profile your Node.js application for 30 seconds
node --prof app.js
# Generate a readable text report
node --prof-process isolate-*.log > processed.txt
# For a visual flame graph, use 0x
npx 0x -- node app.js
# Opens a flame graph in your browser after Ctrl+CMethod 2: 0x (Recommended for Next.js)
pnpm add -D 0x
# Profile your Next.js dev server
npx 0x -- node node_modules/.bin/next dev
# Or profile a specific API route load test
npx 0x -- node -e "
const fetch = require('node-fetch');
for (let i = 0; i < 100; i++) {
fetch('http://localhost:3000/api/checkout', { method: 'POST', body: JSON.stringify({items: []}) });
}
"
# Creates a flame graph HTML fileMethod 3: Clinic.js (Comprehensive Analysis)
pnpm add -D clinic
# CPU profiling with flame graph
npx clinic flame -- node app.js
# Doctor mode: automatically diagnoses common issues
npx clinic doctor -- node app.js
# Bubbleprof: analyzes async operations
npx clinic bubbleprof -- node app.jsProfiling Next.js Server Routes
For profiling specific API routes, use the --inspect flag with Chrome DevTools:
# Start Next.js with the Node.js inspector
NODE_OPTIONS='--inspect' npm run dev
# Open Chrome → chrome://inspect → Open dedicated DevTools for Node
In the DevTools Performance tab:
- Click "Record".
- Trigger your slow request.
- Click "Stop".
- Click "Call Tree" tab to see a table, or switch to "Flame Chart" for the visual.
Interpreting Garbage Collection in Flame Graphs
Watch for GC-related functions in your flame graph:
If you see these patterns:
╔═══════════════════╗
║ (garbage collect)║ ← Wide GC boxes = memory allocation pressure
╚═══════════════════╝
Or frequent thin GC boxes:
╔══╗╔══╗╔══╗╔══╗╔══╗ ← Many small GC pauses = excessive allocation
╚══╝╚══╝╚══╝╚══╝╚══╝This indicates your application is allocating too much memory, causing the garbage collector to run frequently. Common causes: creating many temporary objects in a loop, not reusing buffers, or large JSON parsing operations.
Action Checklist After Reading a Flame Graph
- Identify the widest box — that is your highest-impact optimization target.
- Follow the call chain downward — the leaf function is doing the actual work.
- Check for sequential operations that could be parallelized.
- Look for third-party library calls consuming disproportionate time.
- Check for GC pressure — if GC is wide, reduce allocation in hot paths.
- Benchmark before and after every optimization — flame graphs can lie if the workload changes.
Conclusion
Flame graphs transform performance optimization from an art form into an engineering discipline. They show you exactly where CPU time is spent, so you spend your optimization effort on the code that actually matters. Generate a flame graph before adding any caching, memoization, or algorithmic optimization — you need to know what is slow before you can make it faster. The 10 minutes to set up 0x and run a profile has saved engineers days of guessing in the wrong direction.