cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Allocation profiling

Learn to find which code allocates objects, estimate allocation volume, spot garbage churn, and measure browser memory safely.

By the end, you can
  • 01
    Read allocation evidenceUse timelines and sampling to connect allocations to the function that made them.
  • 02
    Recognize churnSeparate a growing retained heap from short-lived objects that still create GC work.
  • 03
    Measure 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.

Definition

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.

Make two price labelsPop out in the code editor (opens in a new tab)JavaScript
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.

Step through one allocation burst
Step 0 of 4Ready
Your turn: follow the blue line

This is a replay of instrumented example code, not an engine debugger.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function makePriceLabels(items) {  return items.map((item) => ({ text: item.name + ": $" + item.price, price: item.price }));} const labels = makePriceLabels(cart);console.log(labels.length);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

A tiny allocation counterPop out in the code editor (opens in a new tab)JavaScript
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.

Step through a small allocation timeline
Step 0 of 4Ready
Your turn: follow the blue line

This is a replay of the lesson's timeline model, not a recording from a browser engine.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const result = recordAllocations(owners);console.log(result.totals.labels);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Real-life analogyPaper cups and churn

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.

Step through a sampled allocation estimate
Step 0 of 4Ready
Your turn: follow the blue line

This replay runs the lesson's sampling model. A DevTools sampling profile is an estimate, not an exact allocation ledger.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const result = sampleAllocations(owners, 2);console.log(result.estimatedTotal);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Playground: change the sampling interval
Sampling modelJavaScript
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);
Sample result

Recorded: label, label, render, render

Estimated total: 8

Try it yourself

Every 2: 4 recorded events estimate 8 total events. The real model contains 8.

This model makes the trade-off visible. Real profiler samples are useful estimates, not a complete event ledger.

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.

Real-life analogyChecking every tenth bill

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.

Allocation views answer different questions
ViewShowsUse it when
Allocation timelineEvents over time, often with a live recordingUse it when an action starts a burst and you need its moment.
Allocation samplingStatistical samples attributed to call stacksUse it for lower overhead, longer runs, and a likely top allocator.
Heap snapshotObjects still retained at one momentUse 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.

Create three temporary labelsPop out in the code editor (opens in a new tab)JavaScript
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.

Reuse one small bufferPop out in the code editor (opens in a new tab)JavaScript
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.

Different kinds of memory work
PatternWhat happensWhen to use the evidence
New object in a loopCreates one fresh object on every turnGood when the object is clear and profiling shows no meaningful pressure.
Reuse one bufferChanges fields on one existing objectConsider only for a measured hot path with simple ownership.
Retained cacheKeeps values reachable across actionsInspect with a heap snapshot when the cache should have released data.
Modeled collectionReclaims waiting garbage at a chosen intervalTeaching aid only; engines choose collection timing themselves.
Model waiting garbageJavaScript
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.

Feature-detect browser memoryPop out in the code editor (opens in a new tab)JavaScript
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.

Node Inspector sampling probeJavaScript
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.

Choose the right memory tool
  • 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.
Try it yourself
0 of 6 correct

Sort each investigation by the view that answers its first question.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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

Exercise 1 · Warm-upPredict a label count

Read the cart and the mapper before running it. Type the exact printed number, then say why the callback runs that many times.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const cart = [{ name: "tea", price: 3 }];
const labels = cart.map((item) => ({ text: item.name, price: item.price }));
console.log(labels.length);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upEstimate sampled events

    This is a deliberately even sampling example. Predict the estimate before running it, then compare the sampled count with the total number of owners.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const owners = ["label", "render", "label", "render"];
    const sampled = owners.filter((_, index) => (index + 1) % 2 === 0);
    console.log(sampled.length * 2);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCount churn

      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.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      let created = 0;
      for (let index = 0; index < 3; index += 1) {
        const label = { text: "item " + index };
        created += 1;
      }
      console.log(created);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePick a view

        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.

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName the memory requirement

          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.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real app

            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.

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              Allocation profiling quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does map allocate in the price-label example?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log([{ text: "tea" }, { text: "bread" }].length);

                Choose an answer to see the explanation.

              3. Question 3 of 8Why is allocation sampling useful?

                Choose an answer to see the explanation.

              4. Question 4 of 8What is churn?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does measureUserAgentSpecificMemory() return?

                Choose an answer to see the explanation.

              6. Question 6 of 8Which tool best explains an object still retained after navigation?

                Choose an answer to see the explanation.

              7. 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.

              8. Question 8 of 8What does the reusable buffer print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const 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.

              CompleteFrontend Clear concepts. Working examples.