completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Memory and speed

How garbage collection pauses, code that makes too many objects, low-memory phones and tab discards slow pages down or wipe them, measured in Chrome.

By the end, you can
  • 01
    Find clean-up pauses in your framesRead Minor GC entries in a trace, and tell why a short pause still costs a frame when the frame is already full.
  • 02
    Make less garbage in busy codeReuse arrays, make labels once, and keep code that runs every frame free of new objects.
  • 03
    Keep the visitor's work when a tab is thrown awaySave on visibilitychange, read it back when the page starts, and count discards with document.wasDiscarded.

Why did Ben's shelf stutter, and where did his cart go?

Ben shops at the Leaf and Kettle on a cheap phone. He opens All teas. The page shows a shelf of tea cards, three at a time. He taps › to slide the shelf on to the next three teas. The cards should glide across in 0.6 seconds. This lesson calls each slide a glide.

On his phone, the shelf doesn't glide smoothly. It jerks a little as it moves. Then Ben adds three teas to his cart. He switches to WhatsApp to ask Meera which tea she wants. When he comes back, the page loads again from the top. His cart is empty.

So Ben has two questions. Why does the shelf jerk? And where did his cart go? Both answers are about memory. Memory is the space where a page keeps its data while it runs: every list, every object, every bit of text.

First, how a slide works. The screen shows a quick run of still pictures, called frames. On a 60 Hz screen, a smooth slide needs a new frame every 16.7 ms, as lesson 6.2 showed. A millisecond (ms) is a thousandth of a second. When a frame isn't ready in time, the browser skips it, and the slide jumps.

Ravi built two versions of the shelf. They look the same and show the same cards. The first makes new objects for every tea, in every frame. The second makes its objects once and reuses them. The course recorded Ben's taps on each. Here is what Ben saw on his second tap:

Real frames of Ben's tea shelf on his second tap: before the tap, +63 ms, gliding, +632 ms, stopped. Below, every new picture Chrome drew in the 650 ms after the tap. Makes new: 35 pictures, 3 frames dropped. Reuses: 38 pictures, 0 frames dropped.before the tapThe tea shelf, before the tap+63 ms, glidingThe tea shelf, +63 ms, gliding+632 ms, stoppedThe tea shelf, +632 ms, stoppedMakes new35 pictures3 dropped×××Reuses38 pictures0 droppedtap+300 ms+600 ms
Real frames from the course's recording on Ben's phone, and below, each new picture Chrome drew after his second tap. Each tick is one picture. Each cross is a frame Chrome dropped.

Each tick is one new picture on Ben's screen. On the top row, the gaps are a little wider, and 3 frames were dropped: 35 pictures instead of 38. Each gap is a small jump in the slide. Here is what all five taps cost:

What Ben's five taps costPop out in the code editor (opens in a new tab)JavaScript
// Ben's five taps on the tea shelf, recorded in Chrome with a 4x slower CPUconst makesNew = { pictures: 178, dropped: 11, cleanups: 79 };const reuses = { pictures: 189, dropped: 0, cleanups: 0 }; for (const [name, shelf] of Object.entries({ makesNew, reuses })) {  console.log(`${name}: ${shelf.pictures} pictures, ${shelf.dropped} dropped, ${shelf.cleanups} cleanups`);}
  • Line 1 is a comment. It says where the numbers come from.
  • Lines 2 and 3 hold three numbers for each shelf. pictures counts the new pictures Chrome put on the screen while the shelf moved. dropped counts the frames it had to skip. cleanups counts how often the browser cleaned up memory. The next section explains what that means.
  • Lines 5 to 7 print one line per shelf: makesNew: 178 pictures, 11 dropped, 79 cleanups and reuses: 189 pictures, 0 dropped, 0 cleanups.

The shelf that makes new objects skipped 11 frames. It also cleaned up 79 times. The shelf that reuses its objects skipped none, and never cleaned up while it moved. Why it matters: a few skipped frames in each slide are enough to make a shop feel cheap.

Real-life analogyA café with one barista

A small café has one barista. She makes every drink, and she washes every cup. Washing one cup is quick. But while she washes, no drink gets made. If the café hands out a fresh cup for every sip, she washes all day, and the drinks come late.

In real life: The one barista
In JavaScript: The page's main thread
In real life: Making each drink
In JavaScript: Running the page's code and drawing each frame
In real life: Washing the cups
In JavaScript: Garbage collection: cleaning up memory
In real life: A fresh cup for every sip
In JavaScript: Code that makes new objects in every frame
The main idea

Every object a page makes takes memory. Later, the browser cleans it up on the same main thread that draws the page. And a phone short of memory can throw away a tab you aren't looking at. So make fewer new objects in code that runs often, and save what the visitor would miss.

How the course recorded it

  • Two shelves: the 3,000 teas from lesson 28.2, three cards in view. Ben taps › five times, once a second.
  • Ben's phone: this computer in Chrome 157, with its processor, the CPU, slowed 4 times. V8, Chrome's JavaScript engine, got the smaller space for new objects that it uses on Android phones. The section on phones explains it.
  • The same other work: each frame of both shelves also runs work(), the course's stand-in loop, for the rest of the frame's drawing. It took about 11.9 ms a frame.
  • Real taps: mouse presses sent through Chrome's DevTools protocol, which Chrome passes straight to the page.
  • Three visits each, taking turns. The lesson quotes the middle visit, by frames dropped.
  • The cart: Chrome's own test page, chrome://discards, threw Ben's tab away for real.

Cleaning up memory pauses the page

JavaScript gives memory back by itself. The part of the engine that does it is the garbage collector, or GC. It finds objects that no code can reach any more, and frees their space. Each run of it is a garbage collection. This lesson calls it a clean-up.

How the GC decides what to keep is taught in the JavaScript course: Memory management & garbage collection and Why garbage collection?. Here, we look at what Ben feels.

V8 has two kinds of clean-up. DevTools shows them by name:

  • Minor GC cleans only the young generation: the small space where new objects start. Most objects die young, so this is quick. Generational GC explains why.
  • Major GC cleans the whole heap, all the memory that holds the page's objects. Helper threads do much of its work while the page runs. But it still has to pause the page at the end.

Both stop the main thread for a while. That's the one thread that runs your code and draws the page. V8 has helper threads that share some of the work. It also tries to clean up in quiet moments between frames. You can read how in Incremental and concurrent GC. But some of the work always happens on the main thread, while the page waits.

Real-life analogyA sushi belt

At a sushi bar, the belt never waits. An empty slot passes the chef every few seconds. If a plate is ready, it goes on. If not, the slot goes by empty, and the plate waits for the next one. A chef who stops to wipe the counter only loses a moment. But if he was already late, that moment costs a slot.

In real life: A slot on the belt, every few seconds
In JavaScript: A frame, every 16.7 ms
In real life: The chef
In JavaScript: The main thread
In real life: A plate ready in time
In JavaScript: A frame drawn on time
In real life: An empty slot going past
In JavaScript: A dropped frame
In real life: Wiping the counter between plates
In JavaScript: A clean-up in the middle of a frame

Here is one frame of Ben's second glide on the shelf that makes new objects:

One frame, part by partPop out in the code editor (opens in a new tab)JavaScript
// One frame of Ben's second glide on the makes-new shelf, in msconst frame = { making: 3.5, cleaning: 1.1, rest: 11.7 };const script = frame.making + frame.cleaning + frame.rest;console.log(`Script: ${script.toFixed(1)} ms. Left to draw the frame: ${(16.7 - script).toFixed(1)} ms.`);
  • Line 2 holds three parts of the frame's script, from the recording. making is the time spent making the card objects. cleaning is a Minor GC that ran in the middle of it. rest is everything else the frame's code did.
  • Line 3 adds them up.
  • Line 4 prints Script: 16.3 ms. Left to draw the frame: 0.4 ms.

The clean-up itself was short. But the frame was nearly full before it started. After the script, the browser still has to work out styles, lay out the cards and paint them, as lesson 6.3 showed. There was almost no time left for that.

That frame was not rare. In the five glides, the shelf cleaned up 79 times. Every one was a Minor GC. The middle one took 0.8 ms, and the longest 1.7 ms. 74 of them ran inside a frame, in the middle of the shelf's drawing code. The helper threads did 0.6 ms of the work in all. The main thread did the rest.

Frames with a clean-up usually had about 16.3 ms of script. Frames without one usually had about 15.5 ms. Here are the first frames of the glide, for both shelves:

The script of the first 7 frames of Ben's second glide, against a 16.7 ms frame. Makes new: 16.2 ms, 15.7 ms with a clean-up, 16.3 ms, 16.6 ms with a clean-up, 15.5 ms, 16.5 ms with a clean-up, 15.7 ms. Reuses: 12.5 ms, 12.3 ms, 12.3 ms, 11.9 ms, 11.7 ms, 11.7 ms, 12.4 ms.Makes new116.2 ms2GC15.7 ms316.3 ms4GC16.6 ms515.5 ms6GC16.5 ms715.7 ms16.7 msReuses112.5 ms212.3 ms312.3 ms411.9 ms511.7 ms611.7 ms712.4 ms16.7 ms
Each bar is one frame's script on Ben's phone. Shaded: the shelf's own drawing code, with GC where a clean-up ran inside it. Plain: the rest of the frame. The dashed line is 16.7 ms, and the browser still has to draw the frame after the script.

On the left, every bar ends close to the line. On the right, every bar ends well before it. Why it matters: one clean-up is rarely long. It hurts when clean-ups come often, inside frames that are already full. On a cheap phone, frames often are.

Pick a glide to compare the two shelves, glide by glide:

Playground: Ben's five glides, both shelves
Glide 2Makes new · Reuses
New pictures on screen
3538
Frames dropped
30
Clean-ups
180
Time cleaning up
10.6 ms0.0 ms
Making the shelf, per frame
3.7 ms0.1 ms
Script, middle frame
15.9 ms12.0 ms
Try it yourself
Glide 2: 3 frames dropped on the makes-new shelf, 0 on the reuses shelf

Clean-ups counts the minor collections that started during the glide. Script is the middle frame's script, before the browser draws the frame.

From the course's recording in Chrome 157, with the CPU slowed 4 times: the middle of three visits for each shelf.

Code that makes new things every frame

Making space for a new object is called allocation. Here is the drawing code of the first shelf. It runs once per frame, so about 60 times a second while the shelf moves:

drawShelf, making new objectsJavaScript
function drawShelf(offset) {  const cards = teas.map((tea, i) => ({    name: tea.name,    x: i * CARD - offset,    sizes: SIZES.map((grams) => `${grams} g £${(tea.price * grams / 100).toFixed(2)}`),  }));  let slot = 0;  for (const card of cards.filter((card) => card.x > -CARD && card.x < VIEW)) show(slot++, card.name, card.x, card.sizes);  hideFrom(slot);}
  • Line 1 starts drawShelf. offset is how far the shelf has slid, in pixels.
  • Lines 2 to 6 make a new card object for each of the 3,000 teas. Each card gets a name and a place.
  • Line 5 also makes a new array of three size labels for each tea, like 100 g £4.50. That's three new strings per tea, per frame.
  • Line 8 keeps only the cards that are on the shelf, in a new array, and shows each one. Line 9 hides the card slots it didn't use.

Count the new things in one frame. There are 3,000 objects, 3,000 arrays, at least 9,000 strings and one more array. The shelf shows five cards at most. Everything else is garbage before the frame is even drawn.

Real-life analogyPrinting the whole menu to read one line

A customer asks the price of mint tea. The waiter prints the whole 3,000-line menu, reads one line, and throws the menu in the bin. Then the next customer asks. Printing takes time, and the bin fills fast. Someone has to stop and empty it.

In real life: Printing the whole menu
In JavaScript: Making a card object for all 3,000 teas
In real life: Reading one line
In JavaScript: Showing the five cards on the shelf
In real life: The bin filling up
In JavaScript: The young generation filling up
In real life: Emptying the bin
In JavaScript: A Minor GC
In real life: One menu, pointing at a line
In JavaScript: One array, made once and reused

The second shelf does the same job without making new objects for the teas in each frame:

drawShelf, reusing one arrayJavaScript
const places = new Float64Array(teas.length);const sizes = teas.map((tea) => SIZES.map((grams) => `${grams} g £${(tea.price * grams / 100).toFixed(2)}`));function drawShelf(offset) {  let slot = 0;  for (let i = 0; i < teas.length; i++) {    places[i] = i * CARD - offset;    if (places[i] > -CARD && places[i] < VIEW) show(slot++, teas[i].name, places[i], sizes[i]);  }  hideFrom(slot);}
  • Line 1 makes one list of numbers, places, once, when the page starts. A Float64Array is a list of numbers with a fixed length. Writing into it makes nothing new.
  • Line 2 makes every tea's size labels once, also when the page starts.
  • Lines 3 to 10 are the new drawShelf. Line 6 writes each tea's place into the same array, every frame.
  • Line 7 shows a tea if it's on the shelf, with its labels from line 2.

Both loops visit all 3,000 teas, and both show the same cards. Only the first makes new objects as it goes. Read the table across: each row is one thing the recording measured, for each shelf.

The two shelves on Ben's phone
What the recording measuredMakes new objectsReuses one array
Time making the shelf, per frame3.7 ms0.1 ms
Clean-ups during the five glides790
Time cleaning up, in all58.8 ms0.0 ms
Script of the middle frame15.9 ms12.0 ms
Frames dropped110
New pictures on Ben's screen178189
The JS heap during the glides1.3 to 5.2 MB1.6 to 1.9 MB
Ben's slowest tap, as INP counts it64 ms56 ms

Look at the last row. Ben's taps were quick on both shelves, well under the 200 ms line for a good INP, Interaction to Next Paint (lesson 26.1). INP times each tap only until the next frame. The dropped frames came later in each glide, so INP can't see them. To catch a jerky slide, look at the frames.

Making the objects cost more time than cleaning them up: about 3.7 ms in every frame, against less than a millisecond about every other frame. V8's team says that short-lived objects are cheap to clean up, because a Minor GC only pays for the objects that survive. The recording agrees. The trouble is the making, and how often the clean-ups come.

The heap tells the same story. Here is its size during Ben's second glide, each time Chrome noted it:

The JavaScript heap during Ben's second glide, each time Chrome noted it. Makes new: between 1.3 and 3.3 MB. Reuses: between 1.8 and 1.8 MB.Makes new1.3 to 3.3 MBReusesabout 1.8 MBtap+300 ms+600 ms
The heap's size each time Chrome noted it during Ben's second glide, from the recording's trace. Each drop on the top row is a clean-up. The bottom row hardly moves.

The first shelf fills a few megabytes in two or three frames. Then a clean-up empties it. The line looks like the teeth of a saw. The second shelf barely moves. Why it matters: you can see this in DevTools. Tick Memory in the Performance panel. A JS heap line that climbs and drops again and again during a slide means code is making lots of objects that die young.

A few habits for code that runs on every frame, every scroll or every key:

  • Make lists and labels once, outside the loop, and reuse them.
  • Write into an array you already have, instead of building a new one with map or filter.
  • Look up only what is on screen: the five teas you show, not all 3,000. Lesson 28.3 does this for long lists.
  • Measure first. Code that runs once, on a click, rarely needs this care.

Try both ways in your own browser. The lab runs the two functions on 3,000 teas for 60 frames:

Playground: draw the shelf in your own browser
The lab's two drawShelf functionsJavaScript
// Makes new objects every frame
function drawMakingNew(offset, show) {
  const cards = teas.map((tea, i) => ({ name: tea.name, x: i * CARD - offset, sizes: sizesOf(tea) }));
  let slot = 0;
  for (const card of cards.filter((card) => card.x > -CARD && card.x < VIEW)) show(slot++, card.name, card.x, card.sizes[1]);
  return slot;
}

// Reuses one array and labels, made once
const places = new Float64Array(teas.length);
const sizes = teas.map(sizesOf);
function drawReusing(offset, show) {
  let slot = 0;
  for (let i = 0; i < teas.length; i++) {
    places[i] = i * CARD - offset;
    if (places[i] > -CARD && places[i] < VIEW) show(slot++, teas[i].name, places[i], sizes[i][1]);
  }
  return slot;
}
Ben's shelf, in miniatureMakes new
Try it yourself
Pick a way to draw, then press Glide 60 frames

Both ways look at all 3,000 teas on every frame and show the same cards. Only one makes new objects as it goes.

Live in this browser: real requestAnimationFrame frames. The numbers change from run to run, and a fast computer may drop none.

Phones with little memory

Ben's phone has much less memory than a laptop. That changes two things.

First, clean-ups come more often. V8 sizes the young generation by how much memory the device has. The young generation has two halves. On a computer, each half can grow to 32 MB. On Android phones with less than 8 GB of memory, it stops at 8 MB or less. The same code fills a smaller space sooner, so it cleans up more often. The recording gave V8 the same 8 MB limit, with V8's own flag, --max-semi-space-size.

Second, the phone closes things to free memory. Android runs a low memory killer. When memory runs short, it ends the processes that matter least. Apps you aren't looking at go first. A Chrome tab runs in a process too, so a tab in the background can be closed this way. Android's own guide says that the less memory an app holds in the background, the better its chances to survive.

Real-life analogyA small shared fridge

Four flatmates share one small fridge. When it's full and someone brings new shopping, the old leftovers at the back go in the bin. Nobody asks whose they were. In a bigger fridge, they would still be there.

In real life: The fridge
In JavaScript: The phone's memory
In real life: The flatmates
In JavaScript: Apps and tabs
In real life: Old leftovers at the back
In JavaScript: Tabs and apps in the background
In real life: Leftovers going in the bin
In JavaScript: A tab closed, to be loaded again later

Can a page tell that it's on such a phone? navigator.deviceMemory gives the device's memory in GB. It's rounded, so it says less about who you are. Chrome on Android says 1, 2, 4 or 8. Chrome on a computer says 2 to 32.

A lighter shelf on a small phoneJavaScript
const memory = navigator.deviceMemory ?? 4; // in GB, rounded
const teasPerShelf = memory <= 2 ? 300 : 3000;
console.log(`This shelf will hold ${teasPerShelf} teas.`);
  • Line 1 reads the memory, or uses 4 if the browser doesn't say. Firefox and Safari don't.
  • Line 2 picks a smaller shelf on a phone with 2 GB or less.
  • Line 3 prints how many teas the shelf will hold.

One warning about testing. DevTools can slow the CPU, but it can't shrink the memory. With the CPU 4 times slower, the recorded page still read navigator.deviceMemory as 16, the computer's own memory. Why it matters: to see memory trouble as Ben does, test on a real cheap phone.

When the browser throws a tab away

Back to Ben's cart. When he switched to WhatsApp, his tab went into the background. The Page Lifecycle API names the states a page goes through there:

  • Hidden: not on screen. Its code can still run. The page hears visibilitychange.
  • Frozen: the browser stops its timers and tasks, to save power. The page hears freeze first.
  • Discarded: the browser throws the page away, to free memory. The tab and its title stay, but the page is gone. Nothing runs, so the page hears nothing.

When Ben opens the tab again, the browser loads the page from the start. The new page can read document.wasDiscarded. It's true when the page before it was thrown away.

Real-life analogyA desk cleared while you're out

You work at a library desk with your notes spread out. You step out for a call. The library is busy, so the staff clear your desk for someone else, and leave a slip with your name. When you come back, the desk is empty. Only what you put in your locker is still there.

In real life: Your notes on the desk
In JavaScript: The page and its data, in memory
In real life: Stepping out for a call
In JavaScript: The tab going into the background
In real life: Staff clearing the desk
In JavaScript: The browser discarding the page
In real life: The slip with your name
In JavaScript: The tab's title, and document.wasDiscarded
In real life: Your locker
In JavaScript: sessionStorage

The course did this to Ben's tab for real. Ben added three teas. Then another tab came to the front, like his WhatsApp. Chrome's test page, chrome://discards, froze his tab, then threw it away. Then the tab loaded again. There were two versions of the cart page. The first keeps the cart in a variable only: const cart = [];. The second also saves it:

Save the cart when the page is hiddenJavaScript
const cart = JSON.parse(sessionStorage.getItem("cart") ?? "[]");document.addEventListener("visibilitychange", () => {  if (document.visibilityState === "hidden") {    sessionStorage.setItem("cart", JSON.stringify(cart));  }});
  • Line 1 reads the saved cart when the page starts. sessionStorage keeps text for this tab, even when the page is thrown away. If nothing was saved, ?? "[]" gives an empty list.
  • Line 2 listens for the page being hidden or shown.
  • Lines 3 and 4 save the cart, as text, each time the page is hidden.

Here is what Ben saw, from the recording:

Ben's cart tab, from the recording. Before he left: Cart: 3. Then the tab was hidden (it heard visibilitychange), frozen (it heard freeze) and discarded (nothing ran). When it loaded again, the forgets version showed Cart: 0 and the remembers version showed Cart: 3.Before Ben leftBen's cart page: Cart: 3, In your cart: Mint tea, Earl Grey, Jasmine1. hiddenvisibilitychange2. frozenfreeze3. discardednothing runsForgetsBack on the forgets page: Cart: 0, Your cart is empty.Cart: 0RemembersBack on the remembers page: Cart: 3, In your cart: Mint tea, Earl Grey, JasmineCart: 3
Real frames from the recording. Both pages heard visibilitychange and then freeze, and nothing at the discard. Only the page that saved when it was hidden got the cart back.

The page wrote down each event it heard. First visibilitychange, then freeze. At the discard, nothing. When the tab loaded again, document.wasDiscarded was true. The first page came back with Cart: 0. The second came back with Cart: 3: Mint tea, Earl Grey, Jasmine.

Why it matters: there's no event to catch at the moment of a discard. So save when the page is hidden. Chrome's team warns not to wait for pagehide, beforeunload or unload, because they don't fire on a discard.

Count the visits that came back after a discardJavaScript
if (document.wasDiscarded) {
  // Chrome threw this page away while it was hidden, then loaded it again
  console.log("Welcome back. Your cart was saved.");
}
  • Line 1 checks document.wasDiscarded once, when the page starts.
  • Line 2 is a comment. It says what a true value means.
  • Line 3 tells Ben his cart is safe. A real page could also send the fact to its analytics, to count how often it happens.

Chrome on a computer is careful about what it throws away. After Ben typed a note for the gift card, chrome://discards gave two reasons not to discard his tab: Tab has form interactions and The user has edited the tab's content. The discard didn't happen. But a phone's low memory killer doesn't read forms. Save typed text too.

Memory Saver is a Chrome setting on computers that throws away tabs you haven't used for a while. Its help page lists partly filled forms among the things that keep a tab active.

Try saving and starting again in this tab:

Playground: a cart that survives a fresh start
Cart: 0Saves nothing
In the cart
Nothing yet
Saved the last time this tab was hidden
Nothing
document.wasDiscarded in this tab
…
navigator.deviceMemory in this tab
…
Try it yourself
Cart: 0

Add teas, switch to another tab and back, then press Start the page again. This lab can't make Chrome throw the tab away. It does what a page loaded again would do: it starts from nothing and reads back only what was saved.

Live in this tab. The lab saves in this tab's sessionStorage, and only when you have picked Also save when hidden and the tab is hidden.

When a page runs out of memory

A discard throws away a page you weren't looking at. Running out of memory can end the page you are looking at.

Each tab may only use so much memory. If a page keeps growing past that, its tab crashes, like a suitcase that bursts when you keep pushing more in. Chrome then shows its crash page: "Aw, Snap! Something went wrong while displaying this webpage." If it happens again, Chrome can say: "Try closing other tabs or programs to free up memory."

The course didn't crash a tab on purpose. The words above come from Chrome's own source. The usual cause is a page that grows and never lets go: a list that only gets longer, a cache with no limit, or listeners that keep old elements alive. That's a leak, and the next lesson finds leaks with heap snapshots. Long before a crash, a growing page gets slower. Each Major GC has more to look through, and the phone closes more of the things around it.

  • Load data a page at a time, not a whole year of orders at once.
  • Drop what the visitor can't see any more, so the GC can free it.
  • Measure memory from real visits. Chrome has performance.measureUserAgentSpecificMemory(), and an older, non-standard performance.memory. Lesson 29.3 covers both.

Which memory problem is it?

Each report below comes from a real kind of memory trouble. Sort it by its cause:

Which memory problem is it?
  • A trace shows a Minor GC inside many frames of a scroll
  • The JS heap line climbs and drops again and again during an animation
  • After taking a photo, a phone visitor comes back to a page that loads from the top
  • Analytics show document.wasDiscarded is true for some page views
  • A dashboard left open all day ends on an Aw, Snap! page
  • Loading a whole year of orders into one array crashes the tab on cheap phones
Try it yourself
0 of 6 correct

Sort each report by what causes it.

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

Common mix-ups

  • "Garbage collection only runs when the page is idle." No. V8 tries to use quiet moments, but 74 of the 79 clean-ups ran in the middle of the shelf's drawing code.
  • "Objects that die young are free." No. Each clean-up was short, but making the objects took about 3.7 ms a frame, and the clean-ups came about every other frame.
  • "A slow CPU in DevTools shows me a phone's memory." No. It only slows the CPU. The page still saw 16 GB.
  • "A good INP means smooth animation." No. INP times each tap until the next frame. Both shelves had a good INP, and one still dropped 11 frames.
  • "pagehide will save the cart before a discard." No. It doesn't fire on a discard. Save on visibilitychange.
  • "A discarded tab is a closed tab." No. Its title stays in the tab strip. When you go back, the page loads again, with document.wasDiscarded set to true.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upCount the cards

This tiny shelf has two teas. How many card objects does it make in 36 frames?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const teas = [{ name: "Mint tea", price: 4.5 }, { name: "Green tea", price: 4 }];
let made = 0;
function drawShelf(offset) {
  const cards = teas.map((tea, i) => {
    made++;
    return { name: tea.name, x: i * 124 - offset };
  });
  return cards.filter((card) => card.x < 388).length;
}
for (let frame = 0; frame < 36; frame++) drawShelf(frame * 10);
console.log(made);

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

    Exercise 2 · Warm-upA phone with 3 GB

    This function rounds memory the way Chrome does, with Chrome's limits for Android. What does it print for a phone with 3 GB?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function deviceMemory(megabytes, lowest, highest) {
      let power = 0;
      for (let rest = megabytes; rest > 1; rest = Math.floor(rest / 2)) power++;
      const lower = 2 ** power, upper = 2 ** (power + 1);
      const near = megabytes - lower <= upper - megabytes ? lower : upper;
      return Math.min(highest, Math.max(lowest, near / 1024));
    }
    console.log(deviceMemory(3072, 1, 8)); // an Android phone with 3 GB

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

      Exercise 3 · PracticeSaved too late

      This page saves the cart in pagehide. What does Ben see when his tab loads again?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const storage = new Map(); // stands in for sessionStorage
      const heard = ["visibilitychange", "freeze"]; // what Ben's tab heard before Chrome discarded it
      let cart = ["Mint tea", "Earl Grey", "Jasmine"];
      for (const event of heard) {
        if (event === "pagehide") storage.set("cart", JSON.stringify(cart));
      }
      cart = JSON.parse(storage.get("cart") ?? "[]"); // Chrome loads the tab again
      console.log(`Cart: ${cart.length}`);

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

        Exercise 4 · PracticeThe last glide

        In the five-glides playground, how many clean-ups did the makes-new shelf run in glide 5?

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

          Exercise 5 · ChallengeWhose frame was it in?

          Open the course's trace of the makes-new shelf in Chrome's DevTools. Click a Minor GC inside one of the glides. Which JavaScript function was running when it started?

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

            Exercise 6 · ChallengeWhat else keeps a tab

            Chrome's help page for Memory Saver lists things that keep a tab from being thrown away. Name one, other than a partly filled form.

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

              Check your understanding

              For each question, ask which cost it's about: making objects, cleaning them up, or a tab thrown away.

              The memory and speed quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does this cart code print after Chrome discards the tab and loads it again?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const storage = new Map(); // stands in for sessionStorage
                let cart = ["Mint tea", "Earl Grey", "Jasmine"];
                storage.set("cart", JSON.stringify(cart)); // visibilitychange: the page is hidden
                cart = JSON.parse(storage.get("cart") ?? "[]"); // Chrome discarded the tab, then loaded it again
                console.log(`Cart: ${cart.length}`);

                Choose an answer to see the explanation.

              2. Question 2 of 7Which event does a page receive at the moment Chrome discards its tab?

                Choose an answer to see the explanation.

              3. Question 3 of 7On the makes-new shelf, each clean-up took about a millisecond. Why did frames still get dropped?

                Choose an answer to see the explanation.

              4. Question 4 of 7Both shelves had a good INP in the recording, yet one stuttered. Why didn't INP show the stutter?

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does the same code clean up more often on a cheap Android phone?

                Choose an answer to see the explanation.

              6. Question 6 of 7Does slowing the CPU in DevTools also give the page a phone's small memory?

                Choose an answer to see the explanation.

              7. Question 7 of 7In the recording, Ben typed a note on the cart page. Why wouldn't Chrome discard that tab?

                Choose an answer to see the explanation.

              Key takeaways

              • Every new object costs twice: once to make it, and once to clean it up on the main thread.
              • One clean-up is short. Many clean-ups inside full frames drop frames.
              • In code that runs every frame, reuse arrays and make labels once.
              • Phones with little memory clean up more often, and close tabs in the background.
              • Save the visitor's work on visibilitychange, and read document.wasDiscarded when the page starts.
              • A page that keeps growing ends on a crash page.

              Remember the one-liner.
              Make less in busy code, and save what the visitor would miss.

              Sources

              The facts on this page come from these docs, specs and source files, and from the course's recordings:

              Coming next: Finding leaks in web apps. A page that only grows, and how to find what keeps it growing.

              CompleteFrontend Clear concepts. Working examples.