Allocation profiling
Learn to find which code allocates objects, estimate allocation volume, spot garbage churn, and measure browser memory safely.
- 01Find who makes memoryUse a timeline or sampling to tie new values to the function that made them.
- 02Spot churnTell memory that keeps growing apart from many short-lived objects that still give the garbage collector extra work.
- 03Measure with careCheck that the browser's memory function exists before you use it, and read a small Node sampling check.
What allocation profiling tells you
When your code makes a new object, array, or string, JavaScript has to find some memory to keep it in. Making a new value like this is called an allocation.
Allocation profiling means recording which code makes new values, and when. A profile is the report a tool gives you after it watches your code run. It answers the question "Which code made all this?" before you change any code.
An allocation can be:
- an object, such as
{ text: "tea" }; - an array;
- a string;
- a closure: a function that remembers the variables around it;
- a value the engine makes for its own use.
This lesson looks at the ones you can see in your own code: object literals, arrays, and callbacks that make new values while an action runs.
The previous lesson used a heap snapshot. The heap is the part of memory where objects live. A snapshot is a picture of every object that is still kept right now. It answers a different question: "What is still in memory?"
Allocation profiling looks earlier, while values are being made. So it can tie a burst of new values to one click, one page update, one network request, or one scroll.
Allocation profiling finds the code that makes memory. A heap snapshot finds what still keeps memory. You often use both: first the profile, then the snapshot.
The previous lesson, Heap snapshots in depth, shows how to find what keeps an object alive. This lesson shows when a big number of short-lived values is itself the clue.
Count new objects in a small example
Start with a cart array that holds two items. The code below turns each item into a price label. It is small enough to count the new objects by hand, before you open 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 by line:
- Line 1 makes the input array, with tea and bread.
- Line 2 calls
map.mapruns a function once for each item and collects the results in a new array. The function you pass to it is called a callback. - Each time the callback runs, its object literal (the part in
({ ... })) makes one new label object. - Line 3 prints
2, because the new array holds two labels.
map does not change or copy the cart. The callback runs once for tea and once for bread. Each run makes one fresh object. This is normal, useful work. It is not a bug.
map leaves the cart as it is. It makes a new array, plus one new label object for each item: three new objects in all.This replays the lesson's own example code. It is not a view inside the JavaScript engine.
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 replays this lesson's own code. It is not a view inside the JavaScript engine. It counts only the objects our example asks for. A real profile shows much more, such as values the engine makes for its own use.
Allocation timelines: when memory is made
An allocation timeline lists each allocation in the order it happened, along a line of time. Use it when one action starts the problem. It lets you see the burst right next to that action.
For example, one click on a checkout button might:
- build price labels;
- draw a receipt on the page;
- update the number on the cart icon.
A timeline shows whether the label work happened during the click, or whether some later task made 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 pretend allocations, in time order. Each entry is the name of the function that made it.
- Line 3 goes through the list from first to last.
- Line 4 adds one to that name's count.
- Line 6 prints
3, becausemakePriceLabelsappears three times.
This small counter is a teaching model, not DevTools. DevTools is the set of developer tools built into the browser. A real timeline also records the exact time of each allocation and the functions that were running. The model keeps only the order. That is enough to spot a name that repeats.
makePriceLabels shows up three times, so the code prints 3.This replays the lesson's timeline model. It is not a recording from a browser engine.
script
const result = recordAllocations(owners);console.log(result.totals.labels);Start with one action you can repeat exactly. Press the same button, type the same search text, or open the same page. Then the next recording is easy to compare with this one.
Values your code can no longer reach are called garbage. The garbage collector (GC) is the part of the engine that finds garbage and frees its memory.
Using one paper cup is not a problem. Using many cups quickly fills the bin, even if you only hold one cup at a time. Someone still has to empty the bin.
- In real life: You use one paper cup for a short drink
- In JavaScript: One short-lived object is soon not needed
- In real life: Many cups fill the bin
- In JavaScript: Many allocations leave a lot of garbage
- In real life: Someone empties the bin
- In JavaScript: The GC frees values nothing can reach
Allocation sampling: a lighter guess
Allocation sampling writes down only some allocations and uses them to guess the full pattern. For example, it might note every second allocation and skip the rest.
Because it records less, it slows the page down less. That makes it a good fit for a longer session. The cost is that you no longer get a full list of every allocation.
Sampling is useful when you don't need the exact moment of each object. For example, you may only want to know which of these jobs makes the most short-lived values:
- drawing labels;
- formatting prices;
- reading answers from the server.
This replays the lesson's sampling model. A real sampling profile gives an estimate, not an exact list of every allocation.
script
const result = sampleAllocations(owners, 2);console.log(result.estimatedTotal);The step-through uses six pretend allocations and keeps every second one. It keeps three and guesses 3 × 2 = 6 allocations in total. The guess is exact here only because the input is perfectly even. A real profile gives an estimate: a close guess, not an exact count.
every = 2, only every second allocation is written down: label, label, render. Three kept allocations times two gives a guess of six.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);Kept: label, label, render, render
Guessed total: 8
Real total: 8
Keep every 2: 4 kept × 2 = a guess of 8. The playground really has 8 allocations.
In the playground, change the number in the box from 2 to 3 or 4. The playground uses eight pretend allocations. Compare the estimated total with those eight. A bigger number records fewer allocations. The profiler gets lighter, but the guess gets rougher.
A shop owner checks every tenth bill instead of every bill. They can still learn which items sell the most. Checking fewer bills saves time, and the owner accepts that some bills go unchecked.
- In real life: A shop owner checks every tenth bill
- In JavaScript: A profiler writes down one allocation out of every few
- In real life: Items people buy a lot still show up often
- In JavaScript: Functions that allocate a lot still show up in the samples
- In real life: Some bills are never checked
- In JavaScript: Some single allocations are not in the samples
Pick the right DevTools view
Timelines and sampling answer two questions that sit close together. A heap snapshot answers a third one. A leak is memory that stays kept after it should have been freed.
Pick the view first. That avoids two common mistakes:
- using a snapshot, which shows what is kept, to explain a short burst;
- using a sample to prove that one object leaked.
Read each row as: the view, what it shows, and when to pick it.
| View | What does it show? | When should you use it? |
|---|---|---|
| Allocation timeline | It shows each allocation in time order, while you record. | Use it when one action starts a burst and you need to know when. |
| Allocation sampling | It writes down some allocations and links each to the function that made it. | Use it for longer runs, when you want a light guess at which function makes the most. |
| Heap snapshot | It shows the objects still kept at one moment. | Use it once you know memory keeps growing. |
Pick a timeline when one click or page change starts the problem and timing matters. Pick sampling when the work runs longer and you want a cheap ranking of the functions most likely to allocate.
Pick a heap snapshot only when memory is still kept after the action should be finished. A snapshot follows the chain of objects that keeps a value alive. It does not replay the burst that made those values.
Churn: many short-lived objects
Churn means making a lot of short-lived objects. It is different from a leak:
- A leak keeps memory reachable, so the memory in use keeps growing.
- Churn may leave the memory in use flat, because the objects die quickly. But the garbage collector still has extra work to do.
That extra work for the garbage collector is called GC pressure.
const labels = [];for (let index = 0; index < 3; index += 1) { labels.push({ text: "item " + index });}console.log(labels.length);- Line 2 starts a loop that runs three times.
- Line 3 makes one new object on each turn and adds it to the array.
- Line 5 prints
3: the number of label objects this small program made.
The array keeps the labels on purpose, so you can count them. In a real churn problem, short-lived labels may be thrown away after one page update. But they still had to be made. And the garbage collector still had to find them later.
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 makes one object to reuse.
- Line 4 changes its
textinstead of making a new object. - Line 5 adds that text, a string, to the array.
- Line 7 prints
3, because the array holds three strings. Only one object was made to hold the label text.
text, so at the end it holds item 2.Reusing one object is not always better:
- It can make code harder to follow.
- It can keep old data around by mistake.
- The saving can be too small to measure.
Compare the two ways only after a profile points to a hot path: code that runs very often, where small savings add up.
Each row below is one way code can use memory. It says what happens and when the evidence matters.
| Pattern | What happens? | When does it matter? |
|---|---|---|
| New object in a loop | It makes one fresh object on every turn. | This is fine when the code is clear and a profile shows no real problem. |
| Reuse one object | It changes the fields of one object that already exists. | Try it only for code that runs very often, after a profile points there. |
| Cache that keeps values | It keeps values reachable from one action to the next. | Check it with a heap snapshot when the cache should have let data go. |
| Pretend clean-up model | It frees waiting garbage every few turns, at a time we pick. | It is only for teaching. Real engines pick their own clean-up times. |
The next code uses a helper from this lesson, modelCollectionPressure, so it is shown to read, not to run. The helper makes seven pretend allocations. It runs a pretend clean-up every three turns.
const report = modelCollectionPressure(7, 3);console.log(report.allocations);console.log(report.collections.join(","));console.log(report.garbageWaiting);It prints three lines:
7: the number of allocations;3,3: each of the two clean-ups freed three objects;1: one object is still waiting for the next clean-up.
This is only a model. Real browsers decide on their own when to collect garbage.
Measure a page's memory
performance.measureUserAgentSpecificMemory() is a browser function that measures how much memory the page uses. It returns a promise: a value that arrives later. When it arrives, the report holds:
bytes: the total number of bytes;breakdown: a list that splits the total into parts.
Use it to compare the same page, in the same state, over time. One number alone is not a memory budget.
Checking that a feature exists before you use it is called feature detection. The code below does that first.
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 function. It may be missing.
- Lines 3 to 5 print
not available hereand stop if the function is missing. - Line 7 waits for the report.
api.call(performance)runs the function onperformance, the same as writingperformance.measureUserAgentSpecificMemory(). - Line 8 prints the total bytes, and line 9 prints how many parts the breakdown has.
The browser only allows this function on a page that is:
- a secure context: served over HTTPS, or from your own computer (
localhost); - cross-origin isolated: a locked-down page. Windows from other sites can't keep a link to it, and files it loads from other sites must agree to be loaded.
The server turns on cross-origin isolation with two response headers, called COOP (Cross-Origin-Opener-Policy) and COEP (Cross-Origin-Embedder-Policy). They are security settings sent with the page. JavaScript can't switch them on from inside a function. Read Browser security model for the full rules.
Feature detection matters, because:
- a browser may not have the function;
- a local preview may not be cross-origin isolated;
- a page shown inside another page may have different permissions.
When the function is missing, keep going with allocation profiles and snapshots. Don't make up a byte count.
A real sampling check in Node
Node is a program that runs JavaScript outside the browser. It has a built-in Inspector: a way for tools to talk to the engine and ask it for profiles.
The code below uses the Inspector to ask for a sampling profile. It only runs in Node, so it is shown here to read, not to run in the browser.
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 to 8 define
makePriceLabels. Each call makes 1,200 label objects. - Lines 10 to 14 connect to the Inspector.
postsends it one command and waits for the answer. - Lines 16 and 17 turn on heap profiling and start sampling.
samplingInterval: 256asks for about one sample for every 256 bytes made, so small objects are likely to be seen. - Line 18 calls
makePriceLabelstwelve times and keeps all the labels until sampling stops. That makes the function very likely to show up. - Line 19 stops sampling and gets the profile. The profile is a tree: each part names a function, and its children are the functions it called.
- Lines 20 to 25 walk the tree and collect every function name.
- Line 26 prints whether
makePriceLabelswas found.
The lesson test checks that true-or-false answer. It does not check a byte count, a memory address, or a time, because those change from run to run.
This proves one small, real fact: Node's sampling profile names the function that made the objects. It does not mean browser and Node profiles look the same. It also does not mean every allocation from this function is a problem.
A practical profiling routine
Start with a problem a user can notice, such as:
- a slow search box;
- scrolling that stutters;
- a page that gets heavier on each visit;
- a server request that makes a surprising number of short-lived values.
Then follow these steps:
- Repeat one action.
- Record an allocation view.
- Name the function that most likely makes the values.
- Decide what it is: normal work, churn during an action, or a lead to check with a heap snapshot.
- Find the new values made during one checkout click.
- Find which functions likely make the most values in a long session.
- Find why a cart array is still kept after you leave the page.
- See whether scrolling makes bursts of new values again and again.
- Guess which function makes the most objects overall.
- Compare what is still kept before and after ten actions.
Sort each task by the view that answers its first question.
Change one cause at a time. For example, remove one step that makes the same values twice. Then record again and compare the same action. Don't swap every object literal for one shared object before you know allocation is the problem.
Common mistakes
Each memory tool has its own job. Keep their questions apart. Then a profile is easier to explain, and you are less likely to make a clever change that doesn't help.
- "Lots of allocations means a leak." A leak keeps objects alive. Objects from churn may be freed soon after they are made.
- "Sampling is wrong because it misses events." Sampling skips allocations on purpose, so it slows the page less. It still gives a good estimate.
- "One byte count is a budget." Compare the same action, on the same setup, over time.
- "Reusing objects always wins." Reuse can make code harder to follow and keep old data alive longer.
A profile is evidence for one question. It is not a final answer. Pair it with an action you can repeat and a simple guess you can test. "This cart code makes new labels on every key press" is much more useful than "memory looks high."
Practice exercises
Read the cart and the map call before you run it. Type the exact number it prints. 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 makes one label object for the one item.
This sampling example is perfectly even. Guess the estimate before you run it. Then compare the number of kept events with the total number of events.
const owners = ["label", "render", "label", "render"];
const sampled = owners.filter((_, index) => (index + 1) % 2 === 0);
console.log(sampled.length * 2);It prints 4. Two kept events times an interval of two gives an estimate of four events.
Count how many objects are made, not just the final number on screen. Type how many label objects this loop makes. Then say 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 turn makes one label object, even though no later code keeps it.
A checkout click makes a burst of new values only while the receipt is drawn. Name the first DevTools view you would record. Then say what question that view answers.
Choose an allocation timeline. One checkout click is one short action, so its timing matters.
On a local preview, the memory example prints not available here. Name the page setting to check before you call this a bug in your app.
The page must be cross-origin isolated before this browser function works.
A product page feels slow after you type in its search box. Write the first thing you would do. Do it before you rewrite the code that builds the results, and before you add a shared cache that reuses objects.
Profile first. Repeat one action, find the function that makes the values, and compare again after one change.
Check your understanding
For each question, first decide which of three things it asks:
- who made the memory;
- when a burst happened;
- what is still kept.
The words sound close, but each tool answers a different one.
Question 1 of 8What new values does
mapmake 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 why an object is still kept after you leave a page?
Choose an answer to see the explanation.
Question 7 of 8The pressure model makes seven allocations and cleans up after every three. How many are still waiting after the second clean-up?
Choose an answer to see the explanation.
Question 8 of 8What does this reused object 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
- An allocation profile shows who makes values. A heap snapshot shows what is still kept.
- A timeline keeps the order of every allocation. Sampling writes down only some allocations and guesses the rest, so it slows the page less.
- Churn, many short-lived objects, can give the garbage collector extra work even when nothing leaks.
- Check that the browser memory function exists before you use it. It needs a cross-origin isolated page.
- Measure first. Then fix the code that makes the values behind the user's problem.
Remember the one-liner.
Allocation profiling asks who makes memory. A heap snapshot asks what still keeps it.
Coming next: Memory in large apps.