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()andnode --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
// 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.
- Open DevTools, the Memory tab.
- Pick Heap snapshot and press Take snapshot (snapshot 1).
- Perform the suspicious actions: SPA navigation, opening and closing modals, lists and so on.
- Press Take snapshot again (snapshot 2).
- Compare: Comparison mode, sorted by Delta Size or Delta Count. Look for growing owners (classes and constructors), Detached DOM nodes, big arrays and
Mapinstances.
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:
- Memory, then Allocation instrumentation on timeline, then Start.
- Replay the scenario for 10-30 seconds.
- 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.
// 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.
node --inspect index.js
# or stopping on the first line
node --inspect-brk index.jsThen 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.
npm i heapdumpimport 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.
node --trace-gc index.jsWatch 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.
| Tool | What it gives | Overhead |
|---|---|---|
| Heap snapshot | the full object graph, Retainers, snapshot diffing | high |
| Allocation instrumentation on timeline | allocation stacks with lines of code | high |
| Sampling allocation profiler | approximate hot allocation spots | low |
| Performance, Memory metric | the shape of the heap over time, the effect of a manual GC | medium |
process.memoryUsage() | rss, heapUsed, heapTotal, external | close to zero |
node --trace-gc | GC pause frequency and duration, the level after GC | low |
SPA and React: the verification protocol
- Open page A, take Heap snapshot 1.
- Go to page B and back to A (or open and close the component), take snapshot 2.
- Press GC (the bin icon), take snapshot 3.
- 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;
EventListenerhandlers withoutremoveEventListener;- live
setTimeoutandsetIntervaltimers; - zombie subscriptions (WebSocket, RxJS);
MaporSetcaches with no TTL and no eviction;- large structures in Context or Redux that nobody ever resets.
React specifics:
-
check every
useEffectfor a cleanup, that is, for a returned function:javascriptuseEffect(() => { 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
refvalues: after unmount they must becomenull.
The leak hunting method
- Fix the symptom in numbers: "in N minutes the heap grows from X to Y".
- Narrow the scenario down to the minimal reproducible sequence of actions.
- Collect data: heap snapshots before and after, an Allocation timeline, a Performance recording with the Memory metric.
- Find the owner: the class or constructor that grows, then read its Retainers.
- Tie it to the code: file and line through Allocation stacks or source maps.
- Fix it: add cleanup, drop the stray references, bound the cache.
- Verify: repeat steps 1-3, the growth must be gone.
- Automate where possible: an e2e script that measures
usedJSHeapSizestep 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
MaporSetgrow: the cache has no TTL and no LRU eviction. - Many
TimeoutandIntervalentries:clearTimeoutandclearIntervalare 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
heapUsedas 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 readyA concise answer to help you respond confidently on this topic during an interview.