Allocation profiling
Learn to find which code allocates objects, estimate allocation volume, spot garbage churn, and measure browser memory safely.
- 01Read allocation evidenceUse timelines and sampling to connect allocations to the function that made them.
- 02Recognize churnSeparate a growing retained heap from short-lived objects that still create GC work.
- 03Measure carefullyFeature-detect browser memory measurement and use a small Node Inspector probe as evidence.
What allocation profiling answers
Allocation profiling records or estimates where code creates values that need memory. It answers “who made this?” before you change a line that only looks expensive.
An allocation can be an object, array, string, closure, or engine-managed value. This lesson focuses on the JavaScript you can see: object literals, arrays, and callbacks that make new values while an action runs.
A heap snapshot answers a different question: what is still retained now. Allocation profiling looks earlier, while values are being created, so it can connect a burst to a click, render, request, or scroll.
Allocation profiling finds the code that creates memory. A retained-memory investigation finds the paths that still keep memory alive. You often use both, in that order.
The previous lesson, Heap snapshots in depth, teaches retainers and retained size. Here you will learn when a large number of short-lived values is itself the useful clue.
Count a small allocation
Start with a cart that has two items. The function below turns every item into a separate price label, which makes the allocation easy to count before opening any tool.
const cart = [{ name: "tea", price: 3 }, { name: "bread", price: 4 }];const labels = cart.map((item) => ({ text: item.name + ": $" + item.price, price: item.price }));console.log(labels.length);Line 1 creates the input array with tea and bread. Line 2 calls map, which creates a new result array and one new label object for each input item. Line 3 prints 2, because the result array has two labels.
The original cart is not copied by magic. The callback runs once for tea and once for bread, and each object literal inside the callback makes a fresh object. That is ordinary useful work, not evidence of a bug.
This is a replay of instrumented example code, not an engine debugger.
script
function makePriceLabels(items) { return items.map((item) => ({ text: item.name + ": $" + item.price, price: item.price }));} const labels = makePriceLabels(cart);console.log(labels.length);The step-through is a replay of this lesson function, not an engine debugger. It makes the boundary clear: we can count the objects our example asks JavaScript to create, while a real profile reports a much richer engine view.
Allocation timelines
An allocation timeline puts allocation events on a time axis. Use it when a specific action starts the problem and you need to see the burst near that action.
For example, a checkout click might build labels, render a receipt, and update a cart badge. A timeline helps you ask whether the label work happened during the click or whether another later task caused the burst.
const owners = ["makePriceLabels", "renderCart", "makePriceLabels", "makePriceLabels"];const totals = {};for (const owner of owners) { totals[owner] = (totals[owner] || 0) + 1;}console.log(totals.makePriceLabels);Line 1 lists four modeled allocation owners in their time order. Line 3 walks through that list from first event to last. Line 5 prints 3, because makePriceLabels appears three times.
This small counter is a teaching model, not DevTools. A browser profile can associate allocations with stack frames and timestamps; the model only keeps the one fact we need first: an event order that points at a repeated owner.
This is a replay of the lesson's timeline model, not a recording from a browser engine.
script
const result = recordAllocations(owners);console.log(result.totals.labels);Begin with one repeatable action. Press the same button, type the same search text, or visit the same route. A profile without a clear action is much harder to compare with the next run.
Using a paper cup once is not a problem. Using many cups quickly fills a bin, even when you only hold one cup at a time. Someone still has to empty that bin.
- In real life: One cup is used briefly
- In JavaScript: One temporary object dies quickly
- In real life: Many cups fill the bin
- In JavaScript: Many allocations make garbage
- In real life: Someone empties the bin
- In JavaScript: GC reclaims unreachable values
Where the analogy stops: A bin has a fixed visible size. Real engine heaps and collection schedules are more complicated.
Allocation sampling
Allocation sampling records some allocations and estimates the larger pattern. It gives up a complete event ledger so that a profile can run with less overhead, especially during a longer session.
Sampling is useful when you do not need the exact moment of every object. You might want to know whether rendering labels, formatting prices, or parsing responses is likely to create the largest share of temporary values.
This replay runs the lesson's sampling model. A DevTools sampling profile is an estimate, not an exact allocation ledger.
script
const result = sampleAllocations(owners, 2);console.log(result.estimatedTotal);The recording keeps every second event in a deliberately even six-event model. It sees three samples and estimates six total events. The estimate happens to be exact here because the input is designed to be even; a real profile is an estimate.
const owners = ["label", "label", "render", "label", "label", "render"];const every = 2;const sampled = owners.filter((_, index) => (index + 1) % every === 0);console.log(sampled.join(","));console.log(sampled.length * every);Recorded: label, label, render, render
Estimated total: 8
Every 2: 4 recorded events estimate 8 total events. The real model contains 8.
Change N in the playground from two to three or four. Compare the estimated total with the eight real modeled events. A larger interval records fewer events, which makes the profiler lighter but the estimate coarser.
A shop owner checks every tenth bill instead of every bill and can still learn which items are most common. Sampling stays lighter by accepting that some individual bills are absent.
- In real life: A shop owner checks every tenth bill
- In JavaScript: A profiler records every Nth allocation
- In real life: Popular items still appear often
- In JavaScript: Hot allocators appear in samples
- In real life: Some bills are not checked
- In JavaScript: Individual allocations can be absent
Where the analogy stops: A real profiler uses its own sampling rules and call stacks, not shop bills.
Choose the DevTools view
Timeline and sampling profiles answer neighboring questions. Choosing the view first prevents a common mistake: trying to use a retained-object tool to explain a short burst, or using a sample to prove a specific object leaked.
| View | Shows | Use it when |
|---|---|---|
| Allocation timeline | Events over time, often with a live recording | Use it when an action starts a burst and you need its moment. |
| Allocation sampling | Statistical samples attributed to call stacks | Use it for lower overhead, longer runs, and a likely top allocator. |
| Heap snapshot | Objects still retained at one moment | Use it after you know memory keeps growing. |
Use a timeline when one click or route transition starts the mystery and timing matters. Use sampling when work lasts longer and you need a lower-overhead ranking of likely allocating functions.
Choose a heap snapshot only when memory remains after the action should have finished. A snapshot follows retainers; it does not replay the allocation burst that created those values.
Churn and GC pressure
Churn is a large number of short-lived allocations. It is different from a leak: a leak keeps memory reachable, while churn can leave retained memory flat and still give the garbage collector frequent work.
const labels = [];for (let index = 0; index < 3; index += 1) { labels.push({ text: "item " + index });}console.log(labels.length);Line 2 repeats three times. Line 3 creates one object on each turn and stores it in the array. Line 5 prints 3, the number of label objects that this small program created.
The array intentionally keeps the labels alive so you can count them. In a real churn problem, temporary labels may disappear after a render, but they still had to be allocated and later discovered as unreachable.
const labels = [];const reusable = { text: "" };for (let index = 0; index < 3; index += 1) { reusable.text = "item " + index; labels.push(reusable.text);}console.log(labels.length);Line 2 creates one reusable object. Line 4 changes its text property instead of making another object. Line 6 prints 3, because the array keeps three string values while the small buffer itself stayed one object.
Reuse is not automatically better. It can make code harder to reason about, accidentally retain stale data, or matter too little to measure. Compare the two approaches only after a profile points to a real hot path.
| Pattern | What happens | When to use the evidence |
|---|---|---|
| New object in a loop | Creates one fresh object on every turn | Good when the object is clear and profiling shows no meaningful pressure. |
| Reuse one buffer | Changes fields on one existing object | Consider only for a measured hot path with simple ownership. |
| Retained cache | Keeps values reachable across actions | Inspect with a heap snapshot when the cache should have released data. |
| Modeled collection | Reclaims waiting garbage at a chosen interval | Teaching aid only; engines choose collection timing themselves. |
const report = modelCollectionPressure(7, 3);console.log(report.allocations);console.log(report.collections.join(","));console.log(report.garbageWaiting);This model makes seven allocations and runs a modeled collection every three turns. It reports 7 allocations, collections of 3,3, and one remaining object waiting for the next collection. It is a model, not a claim about browser GC timing.
Measure page memory
measureUserAgentSpecificMemory is a browser API that returns a promise with a total byte count and a breakdown. It is useful for carefully comparing the same page state, not for declaring a universal memory budget from one number.
async function showMemory() { const api = performance.measureUserAgentSpecificMemory; if (typeof api !== "function") { console.log("not available here"); return; } const report = await api.call(performance); console.log("total bytes", report.bytes); console.log("breakdown entries", report.breakdown.length);} showMemory();Line 2 reads the optional browser API. Lines 3 through 6 print not available here and return when it is missing. Line 8 waits for the report, while line 9 prints its total byte count when the browser allows the measurement.
The API requires a cross-origin isolated page. In plain words, the browser gives your page a more private boundary: outside windows cannot casually share a process relationship with it, and cross-origin resources must explicitly opt in.
The two response headers commonly involved are COOP and COEP. They are security settings, not a JavaScript flag you can turn on from a function. Read Browser security model for the full boundary and resource rules.
Feature detection matters because a browser can lack the API, a local preview can lack isolation, and an embedded page can have different permissions. The fallback is useful information: continue with allocation profiles and snapshots instead of inventing a byte count.
Real sampling evidence
Node exposes the Inspector protocol, which tools use to request heap sampling. The probe below is Node-only, so it is displayed for reading rather than run in the browser sandbox.
import { Session } from "node:inspector"; function makePriceLabels() { return Array.from({ length: 1200 }, (_, index) => ({ text: "tea: $" + index, details: "label " + index + " for the cart", }));} const session = new Session();session.connect();const post = (method, params = {}) => new Promise((resolve, reject) => { session.post(method, params, (error, result) => error ? reject(error) : resolve(result));}); await post("HeapProfiler.enable");await post("HeapProfiler.startSampling", { samplingInterval: 256 });const retainedLabels = Array.from({ length: 12 }, makePriceLabels);const profile = await post("HeapProfiler.stopSampling");const names = new Set();const visit = (node) => { names.add(node.callFrame.functionName); for (const child of node.children || []) visit(child);};visit(profile.profile.head);console.log("found makePriceLabels", names.has("makePriceLabels"));console.log("retained labels", retainedLabels.length);session.disconnect();Lines 3 through 8 name makePriceLabels and allocate a useful batch of labels. Lines 15 and 16 enable heap profiling and start sampling at a small interval. Line 17 keeps twelve batches alive until sampling stops, making the named function much more likely to appear.
Lines 19 through 24 walk the profile tree and collect function names. The child process prints whether it found makePriceLabels; the test checks that stable boolean instead of a byte total, address, or timing that varies from run to run.
This confirms one small real fact about Node Inspector sampling. It does not make the browser and Node profiles identical, and it does not tell us that every allocation from this function is a performance problem.
A practical profiling loop
Start with a user-visible symptom. It might be a slow search, janky scroll, a route that gets heavier each visit, or a server request that creates a surprising amount of temporary data.
Reproduce one action, record an allocation view, and name the likely allocating function. Then decide whether the allocation is expected work, short-lived churn near an interaction, or a lead that deserves a retained-memory check.
- Find allocations during one checkout click.
- Find likely allocators during a long session.
- Find why a cart array stays after navigation.
- See whether scrolling causes repeated allocation bursts.
- Estimate which function creates most objects overall.
- Compare what remains before and after ten actions.
Sort each investigation by the view that answers its first question.
Change one measured cause at a time. For example, reduce one duplicate transformation, record again, and compare the same action. Do not replace every object literal with a shared object before you know that allocation is the problem.
Common mistakes
Memory tools have different jobs. Keeping their questions separate makes a profile much easier to explain to another developer and much less likely to produce a clever but irrelevant optimization.
- “Lots of allocations means a leak.” A leak retains objects; churn may be collected quickly.
- “Sampling is wrong because it misses events.” It is an estimate designed for lower overhead.
- “One byte count is a budget.” Compare the same action and environment over time.
- “Reuse always wins.” Reuse can complicate code and keep old data alive longer.
A profile is evidence for a question, not a verdict. Pair its result with a repeatable action and a simple hypothesis: “this cart mapper makes labels on every keystroke” is much more actionable than “memory looks high.”
Practice exercises
Read the cart and the mapper before running it. Type the exact printed number, then say why the callback runs that many times.
const cart = [{ name: "tea", price: 3 }];
const labels = cart.map((item) => ({ text: item.name, price: item.price }));
console.log(labels.length);It prints 1. The callback runs once and creates one label object for the one item.
This is a deliberately even sampling example. Predict the estimate before running it, then compare the sampled count with the total number of owners.
const owners = ["label", "render", "label", "render"];
const sampled = owners.filter((_, index) => (index + 1) % 2 === 0);
console.log(sampled.length * 2);It prints 4: two recorded events multiplied by an interval of two estimate four modeled events.
Count object creation, not just the final visible value. Type the number of label objects this loop creates and describe where the object literal runs.
let created = 0;
for (let index = 0; index < 3; index += 1) {
const label = { text: "item " + index };
created += 1;
}
console.log(created);It prints 3; each loop turn creates one label object, whether or not later code keeps it.
A checkout click makes a burst only while the receipt renders. Name the first DevTools view you would record, then explain the question that view can answer.
Choose an allocation timeline. A checkout click is one bounded action, so its moment matters.
A local preview prints not available here for the memory API. Name the page-level condition to check before treating that fallback as an application bug.
The page must be cross-origin isolated before this browser API can run.
A product page feels slow after typing in its search box. Write the first action you would take before rewriting the result mapper or adding a reusable global cache.
Profile first. Reproduce one action, identify the allocating function, and compare again after one measured change.
Check your understanding
In each answer, separate three questions: who allocated, when a burst happened, and what remains retained. The words are close together, but the tools answer different parts of a memory investigation.
Question 1 of 8What does
mapallocate in the price-label example?Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictconsole.log([{ text: "tea" }, { text: "bread" }].length);Choose an answer to see the explanation.
Question 3 of 8Why is allocation sampling useful?
Choose an answer to see the explanation.
Question 4 of 8What is churn?
Choose an answer to see the explanation.
Question 5 of 8What does
measureUserAgentSpecificMemory()return?Choose an answer to see the explanation.
Question 6 of 8Which tool best explains an object still retained after navigation?
Choose an answer to see the explanation.
Question 7 of 8The pressure model makes seven allocations and collects after every three. What is left waiting after the second collection?
Choose an answer to see the explanation.
Question 8 of 8What does the reusable buffer print?
Read the code, then predictconst buffer = { text: "" }; buffer.text = "tea"; buffer.text = "bread"; console.log(buffer.text);Choose an answer to see the explanation.
Key takeaways
- Allocation profiles identify who creates values; snapshots identify what remains.
- Timelines preserve time; sampling estimates hot allocation sites with lower overhead.
- Churn can pressure GC even when no retained-memory leak exists.
- Feature-detect browser memory measurement and expect isolation requirements.
- Measure first, then fix the allocation site that matches the user problem.
Remember the one-liner.
Allocation profiling asks who creates memory; heap analysis asks what still owns it.
Coming next: Memory in large apps.