Skip to main content

How to profile memory usage?

In the browser (Chrome DevTools)

1) Basic scenario: "is the heap growing?"

  1. Open DevTools -> Memory.
  2. Choose Heap snapshot -> Take snapshot (snapshot #1).
  3. Perform the suspicious actions (SPA navigation, opening/closing modals, lists, and so on).
  4. Take snapshot again (snapshot #2).
  5. Compare: the Comparison tab -> sort by Δ Size/Δ Count. Look for "growing" owners (classes/constructors), Detached DOM nodes, and large arrays/Maps.

What to look at:

  • Retainers (retention chains) - show which reference is preventing GC.
  • Distance - a small value means close to "root"; such objects are rarely released.

2) Real-time: "where objects are created"

Allocation instrumentation on timeline:

  1. Memory -> Allocation instrumentation on timeline -> Start.
  2. Reproduce the scenario for 10-30 seconds.
  3. Stop -> click through the graph's peaks -> look at the Allocation stacks (the code lines where allocations happen).

Pros: shows the hot spots of allocations "in place". Cons: noisy, but great for catching spikes.

3) Light mode

Sampling allocation profiler - less precise, but almost no overhead. Handy when it's hard to reproduce the bug for a long time.

4) Memory timeline

In the Performance tab, enable the Memory metric -> make a recording. If after a "manual GC" (the trash-can icon) the heap doesn't return to roughly its original level, there's a leak.

5) Quick checks from the console

javascript
// Current heap size (Chromium only): performance.memory.usedJSHeapSize // (Experimental) more detail per agent: if (performance.measureUserAgentSpecificMemory) { performance.measureUserAgentSpecificMemory().then(console.log); }

In Node.js

1) Inspecting via DevTools

Run with the inspector and open it in Chrome:

javascript
node --inspect index.js # or when starting a debug session node --inspect-brk index.js

In Chrome -> chrome://inspect -> Open dedicated DevTools -> the Memory tab -> heap snapshots, just like in the browser.

2) Taking a heap snapshot at runtime

javascript
npm i heapdump
javascript
import heapdump from 'heapdump'; setInterval(() => { heapdump.writeSnapshot(`./heap-${Date.now()}.heapsnapshot`); }, 60_000);

Open the .heapsnapshot file in DevTools and compare.

3) Quick counters

javascript
setInterval(() => { const m = process.memoryUsage(); console.log({ rss: Math.round(m.rss/1024/1024)+'MB', heapUsed: Math.round(m.heapUsed/1024/1024)+'MB', heapTotal: Math.round(m.heapTotal/1024/1024)+'MB', ext: Math.round(m.external/1024/1024)+'MB' }); }, 5000);

If heapUsed keeps sawtoothing upward and never comes back down, that's a suspected leak.

4) GC traces (diagnostics)

javascript
node --trace-gc index.js

Watch the frequency and duration of pauses; a steady rise in "after GC" is a bad sign.

For SPA/React (and components in general)

1) The de-facto standard check protocol

  1. Open page A -> take Heap snapshot #1.
  2. Go to page B, come back to A (or open/close the component) -> #2.
  3. Click GC (the trash-can icon) -> #3.
  4. Compare #1 and #3. If page A's/the component's objects remain, and their count/size grows after repeats, that's a leak.

Look for:

  • Detached DOM nodes;
  • EventListener handlers without a matching removeEventListener;
  • active Timeout/Interval;
  • "zombie" subscriptions (WebSocket, RxJS);
  • caches (Map/Set) with no TTL/cleanup;
  • large structures in Context/Redux that nobody resets.

2) React specifics

  • Check useEffect for cleanup (a returned function):

    javascript
    useEffect(() => { const id = setInterval(fetchData, 5000); return () => clearInterval(id); // required! }, []);
  • React DevTools Profiler: the component should Unmount when leaving the page.

  • Watch refs to the DOM: after unmount they should become null.

A method for finding a leak (briefly)

  1. Pin down the symptom: "after N minutes the heap grows from X to Y".
  2. Narrow the scenario: the minimal reproducible sequence of actions.
  3. Gather data: a Heap snapshot before/after, the Allocation timeline, Perf Memory.
  4. Find the owner: the class/constructor where the growth happens -> check Retainers.
  5. Tie it to the code: the line/file, via Allocation stacks or sourcemaps.
  6. Fix it: add a cleanup / remove references / limit the cache.
  7. Verify: repeat steps 1-3 - the growth should disappear.
  8. Automate it (if possible): an e2e script that measures usedJSHeapSize at each step.

Common "tells" in reports

  • A Detached HTMLDivElement grows -> a listener/reference to the DOM was never removed.
  • Arrays/objects in a Map/Set grow -> there's no TTL/LRU cleanup.
  • Lots of Timeout/Interval -> no clearTimeout/clearInterval.
  • A pile of component instances after navigation -> it doesn't unmount, or closures/subscriptions "hold" it.
  • A leak in a third-party library -> wrap it in your own adapter with cleanup, or replace it.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.