Skip to main content

Profiling memory usage

Memory profiling is measuring how much heap an application holds, which objects live in it and which references stop the garbage collector from freeing them. In the browser this is done in the Memory tab of Chrome DevTools, in Node.js through the inspector, heap snapshots and the process.memoryUsage() counters.

Theory

TL;DR

  • The basic technique: take a heap snapshot before a scenario, replay the scenario, take a snapshot after and diff them in Comparison mode by Delta Size and Delta Count.
  • The Retainers column shows the reference chain that holds an object; Distance shows how close the object is to a GC root.
  • Allocation instrumentation on timeline gives allocation stacks with lines of code, Sampling allocation profiler is cheaper but approximate.
  • In Node.js you have node --inspect, heap snapshots in DevTools, heapdump, process.memoryUsage() and node --trace-gc.
  • The sign of a leak: after a forced GC the heap does not return to its starting level.
  • The usual culprits: detached DOM nodes, listeners that are never removed, live timers, subscriptions without unsubscribe, caches with no size limit.

Quick example

javascript
// Node.js: the cheapest way to see the memory trend setInterval(() => { const m = process.memoryUsage(); const mb = (v) => Math.round(v / 1024 / 1024) + 'MB'; console.log({ rss: mb(m.rss), // the whole resident memory of the process heapUsed: mb(m.heapUsed), // actually occupied inside the JS heap heapTotal: mb(m.heapTotal), // allocated for the heap ext: mb(m.external), // buffers outside the heap }); }, 5000); // If heapUsed climbs monotonically and never comes back down, it is time // to take heap snapshots and find the owner of that growth.

Profiling in the browser (Chrome DevTools)

1) The basic "is the heap growing" scenario.

  1. Open DevTools, the Memory tab.
  2. Pick Heap snapshot and press Take snapshot (snapshot 1).
  3. Perform the suspicious actions: SPA navigation, opening and closing modals, lists and so on.
  4. Press Take snapshot again (snapshot 2).
  5. Compare: Comparison mode, sorted by Delta Size or Delta Count. Look for growing owners (classes and constructors), Detached DOM nodes, big arrays and Map instances.

What to look at inside a snapshot:

  • Retainers, the retaining chains: they show which reference exactly stops GC from freeing the object.
  • Distance: a small value means the object sits close to a root, and such objects are rarely released.

2) Real time: where objects are actually created.

The Allocation instrumentation on timeline mode:

  1. Memory, then Allocation instrumentation on timeline, then Start.
  2. Replay the scenario for 10-30 seconds.
  3. Stop, click the spikes on the chart and read the Allocation stacks, the lines of code where allocations happen.

Upside: you see the hot allocation spots right at their place in the code. Downside: a lot of noise, but for catching spikes this mode is the best.

3) The light mode.

Sampling allocation profiler is less precise but has almost no overhead. It is handy when the bug is hard to reproduce quickly and the profile has to run for a long time.

4) The memory timeline.

In the Performance tab enable the Memory metric and record. If after a manual GC (the bin icon) the heap does not return to roughly its starting level, there is a leak.

5) Quick checks from the console.

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

Profiling in Node.js

1) Inspection through DevTools.

javascript
node --inspect index.js # or stopping on the first line node --inspect-brk index.js

Then open chrome://inspect in Chrome, press Open dedicated DevTools and work in the Memory tab exactly as in the browser.

2) A heap snapshot from runtime.

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

Open the .heapsnapshot files in DevTools and compare them with each other.

3) Quick counters.

process.memoryUsage() every few seconds, as in the example above. If heapUsed saws upwards and never comes back, suspect a leak.

4) GC traces for diagnostics.

javascript
node --trace-gc index.js

Watch the frequency and the duration of the pauses. A steadily rising "after GC" level is a bad sign: the collector runs, but has nothing it is allowed to free.

ToolWhat it givesOverhead
Heap snapshotthe full object graph, Retainers, snapshot diffinghigh
Allocation instrumentation on timelineallocation stacks with lines of codehigh
Sampling allocation profilerapproximate hot allocation spotslow
Performance, Memory metricthe shape of the heap over time, the effect of a manual GCmedium
process.memoryUsage()rss, heapUsed, heapTotal, externalclose to zero
node --trace-gcGC pause frequency and duration, the level after GClow

SPA and React: the verification protocol

  1. Open page A, take Heap snapshot 1.
  2. Go to page B and back to A (or open and close the component), take snapshot 2.
  3. Press GC (the bin icon), take snapshot 3.
  4. Compare snapshots 1 and 3. If the objects of page A or of the component are still there and their count or size grows after repeats, that is a leak.

What to look for:

  • Detached DOM nodes;
  • EventListener handlers without removeEventListener;
  • live setTimeout and setInterval timers;
  • zombie subscriptions (WebSocket, RxJS);
  • Map or Set caches with no TTL and no eviction;
  • large structures in Context or Redux that nobody ever resets.

React specifics:

  • check every useEffect for a cleanup, that is, for a returned function:

    javascript
    useEffect(() => { const id = setInterval(fetchData, 5000); return () => clearInterval(id); // mandatory! }, []);
  • React DevTools Profiler: the component must receive an Unmount when you leave the page;

  • watch DOM ref values: after unmount they must become null.

The leak hunting method

  1. Fix the symptom in numbers: "in N minutes the heap grows from X to Y".
  2. Narrow the scenario down to the minimal reproducible sequence of actions.
  3. Collect data: heap snapshots before and after, an Allocation timeline, a Performance recording with the Memory metric.
  4. Find the owner: the class or constructor that grows, then read its Retainers.
  5. Tie it to the code: file and line through Allocation stacks or source maps.
  6. Fix it: add cleanup, drop the stray references, bound the cache.
  7. Verify: repeat steps 1-3, the growth must be gone.
  8. Automate where possible: an e2e script that measures usedJSHeapSize step by step.

Common hints in profiler reports

  • Detached HTMLDivElement is growing: a listener was not removed, or a reference to removed DOM is still held somewhere.
  • Arrays and objects inside a Map or Set grow: the cache has no TTL and no LRU eviction.
  • Many Timeout and Interval entries: clearTimeout and clearInterval are missing.
  • Plenty of component instances after navigation: the component does not unmount, or closures and subscriptions keep it alive.
  • Growth inside a third party library: wrap it in your own adapter with cleanup, or replace it.

Common mistakes

  • Taking one snapshot and drawing conclusions from it. A leak is visible only when two or three snapshots are compared.
  • Not pressing GC before the control snapshot: some objects are still alive simply because the collector has not run yet.
  • Treating any growth of heapUsed as a leak. V8 does not collect until it has to, so what matters is the level after GC, not the peak before it.
  • Profiling a dev build: source maps, HMR and DevTools themselves retain objects and produce false detached nodes.
  • Relying on performance.memory: it is a non standard Chromium only API whose values are coarsened for security reasons.
  • Profiling in a tab with extensions enabled: they add their own allocations and detached nodes to your report.
  • Hunting a leak without a minimal scenario: the noise from unrelated actions will be louder than the leak itself.
  • Removing the symptom and never verifying the fix with a second profiling run.

Short Answer

Interview ready
Premium

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