Course contentsShow
← Node.js
Lesson 1906 of 1,98017. Performance Tuning and ProfilingFree lesson

Profiling Asynchronous Code with Flame Graphs

You will distinguish between synchronous logic and async task handling within visualized traces.

In this lesson, you will learn how to distinguish between synchronous execution and asynchronous orchestration within the "gaps" of a flame graph to accurately identify performance bottlenecks.

The Challenge of Async Visualization

When you generate a flame graph, you are looking at a sampling of the call stack. In synchronous code, the stack is contiguous: function A calls B, which calls C. This results in a neat, vertical block. However, asynchronous code—like async/await flows or callbacks—breaks this continuity.

Because the event loop pauses execution while waiting for I/O (like a database query or network request), the execution "gap" appears as a missing segment in the stack trace. The flame graph doesn’t just show where your code is running; it shows where it isn't running, which is crucial for identifying latency.

Distinguishing Sync vs. Async Logic

Consider this pattern:

async function processData() {
  const result = await heavyComputation(); // Synchronous block 1
  return await saveToDb(result);          // Synchronous block 2
}

When profiling this, you won't see one continuous wide bar. Instead, you will see:

  1. Synchronous Blocks: Tall, narrow stacks representing the active CPU work (e.g., parsing, data transformation). These contribute to your "hot path."
  2. Asynchronous Gaps: The space between the blocks. If you see a function taking 500ms, but the flame graph shows very little cumulative width for its children, you are likely blocked on an asynchronous task.

If you notice a "staircase" effect where function blocks are fragmented, the execution is yielding to the event loop. If your flame graph shows a deep stack that ends abruptly and restarts elsewhere, you are looking at the boundaries where a Promise resolved and triggered the next microtask.

By analyzing the width of the blocks after an await, you can determine if the delay is caused by your own synchronous logic or if the runtime is idling while waiting for an external resource to resolve.

Key Takeaway: In a flame graph, synchronous logic appears as contiguous, wide blocks of CPU activity, while asynchronous task handling is identified by disjointed stacks separated by intervals of event-loop idling.