completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Custom tracks, annotations, and sharing

Put your own timings in the trace, annotate what you found, and share traces your team can open. The course's store on its own custom track, read by DevTools' trace engine, and an annotated trace you can load.

By the end, you can
  • 01
    Add your own tracksPut your code's work on a row of its own with performance.measure or console.timeStamp, and pick the right one for the job.
  • 02
    Annotate a traceLabel a bar, a stretch of time and a link from a cause to its effect, so the trace explains itself.
  • 03
    Share it safelySay what a saved trace holds, and which save options add more, before you send one.

Your own work, in the trace

Asha's message is still on your desk: Add to cart feels slow on her phone. By now you know why. Every click sorts 200,000 past order prices again. Then it draws all 1,800 teas again. A colleague is going to fix it, and you want to show them what you found.

So you plan to send them the trace. Then you picture them opening it. They see thousands of bars, with names like Task, Function call and Timer Fired. Nothing on the screen says "price index" or "Add to cart". Where should they look? And what does each bar mean for the shop?

A trace knows what the browser did. It doesn't know what your code meant by it. Take one of the store's functions, buildIndex(), the one that sorts the prices. The course added a small file to the store, timings.js, which tells the trace when buildIndex() starts and ends. So one call of it shows up in two places:

One call of buildIndex, two viewsPop out in the code editor (opens in a new tab)JavaScript
// buildIndex at load, from one recording, in milliseconds.const mainTrack = [4.8, 116.4, 72.7, 8.6, 2.2]; // five bars in the flame chartconst storeTrack = 208.4; // one entry on the store's own track const total = mainTrack.reduce((sum, ms) => sum + ms, 0).toFixed(1);console.log(`The Main track shows ${mainTrack.length} pieces, ${total} ms in all.`);console.log(`The Store track shows one buildIndex entry, ${storeTrack} ms.`);
  • Line 1 is a comment. Every time is in milliseconds (ms), thousandths of a second.
  • Line 2 holds the bars the Main track drew for this one call. The Main track's flame chart shows which function ran when. It's drawn from samples: Chrome checks, over and over, which function is running.
  • Line 3 holds the one entry the store's own track drew for the same call.
  • Line 5 adds up the five bars.
  • Line 6 prints The Main track shows 5 pieces, 204.7 ms in all.
  • Line 7 prints The Store track shows one buildIndex entry, 208.4 ms.

The two views agree on the time, to within 4 ms. But they tell the story differently. A sample is a snapshot with no length of its own, so DevTools estimates how long each call ran from the samples around it. Here, that estimate came out as five bars side by side. The Store track shows your view instead: one call, with the name you gave it.

Why it matters: your colleague can find one named bar in a second. Five bars with no story need a detective, like the one in Reading a performance trace.

Getting from a raw trace to one your colleague can read takes three steps. This picture shows all three:

Three steps. One: on your own Store row, buildIndex is one entry of 208.4 ms, while the Main row shows the same call in 5 pieces. Two: a note hangs on the click's task. Three: the saved trace file holds the events, your notes and screenshots of the page.1. Your own rowStorebuildIndexMainone call, two views2. Your notesthe click's taskthe slowclickeventsnotesscreenshotsthe trace file3. Check it, then send it
The lesson in three steps. Put your work on a row of its own, add notes to what you found, then check what the file holds before you send it.
  1. Add your own rows. Your code writes its own entries into the trace, with your names, on tracks of your own.
  2. Leave notes. You label the bar that matters, mark a stretch of time, and draw an arrow from a cause to its effect.
  3. Check, then send. You save the trace, read what the file holds, and only then send it.
Real-life analogyYour own row on the shop's timetable

The back office at Leaf and Kettle keeps a timetable of the day, one row per worker. It shows when the head cook and the counter were busy, but not what for. So you add a row of your own, called Tea bar. In it you write "kettle boiling, 6:02 to 6:06" and "checkout steps, 6:07 to 6:09". Now anyone who reads the timetable sees your work in your own words, lined up with everyone else's.

In real life: The shop's timetable, one row per worker
In DevTools: The trace's tracks: Network, Main, Frames and more
In real life: A row of your own, called Tea bar
In DevTools: A custom track, like the store's Store track
In real life: "Kettle boiling, 6:02 to 6:06", written in it
In DevTools: An entry: a name, a start and an end
In real life: A small note beside it: "3 litres, the big kettle"
In DevTools: Properties and a tooltip on the entry

A custom track is that row: a track in the Performance panel that your code fills, with names you choose. The course recorded the store with timings.js in Chrome, on a phone-sized screen with the CPU 4 times slower. It loaded the page, waited, then clicked Add to cart once. Then DevTools' own trace engine, the code the Performance panel uses to read a trace, read the recording.

The playground shows three rows of it. The Timings row holds a mark at the moment of the click. The Store row holds the store's own four entries. The Main row holds the flame chart's bars for buildIndex(), so you can compare. Pick an entry to see what the Summary tab says about it:

Playground: the store's own track
Leaf and Kettle0 to 3,500 ms
Timings
Store
Main
Summaryrecorded
EntrybuildIndex
Start271.6 ms
Duration208.4 ms
Colorprimary
Prices sorted200000
TooltipSorts past order prices
Made withperformance.measure
Try it yourself
The Summary tab

buildIndex, made with performance.measure. A measure can carry properties and a tooltip, so the Summary tab shows them.

Recorded in Chrome, on a phone-sized screen with the CPU 4 times slower: the store with timings.js, loaded, then clicked once. DevTools' own trace engine read the trace. The course recorded it five times and kept the run in the middle.
The main idea

A trace shows what the browser did. Custom tracks add what your code meant. Annotations add what you found. Whoever opens the trace next sees all three.

User Timing in the trace

Start with a tool every browser has. User Timing is a standard way for your code to write down times, with names. MDN gives the reason in one line: the browser "cannot determine what is going on in your application." So your code tells it. Two calls do most of the work.

A mark is one moment

performance.mark() writes down one moment and gives it a name. It's like pressing the lap button on a stopwatch.

Mark the clickPop out in the code editor (opens in a new tab)JavaScript
// A mark is one moment, with a name.const click = performance.mark("Add to cart"); console.log(`${click.name} is a ${click.entryType}.`);console.log(`It lasts ${click.duration} ms, because it's one moment.`);
  • Line 1 is a comment.
  • Line 2 writes down this moment, named Add to cart. It gives back the entry it made, and the code keeps it as click.
  • Line 4 prints Add to cart is a mark. Every entry has a type, and this one's type is mark.
  • Line 5 prints It lasts 0 ms, because it's one moment. A mark has no length, so its duration is always 0.

The moment itself is in the mark's startTime, counted in milliseconds from the start of the page.

A measure is a stretch of time

performance.measure() writes down a stretch of time, from a start to an end. If a mark is one press of the lap button, a measure is the time between two presses.

Measure the sortPop out in the code editor (opens in a new tab)JavaScript
// A measure is a stretch of time, from a start to an end.const start = performance.now();// ... the store sorts its 200,000 prices here ...const built = performance.measure("buildIndex", { start }); console.log(`${built.name} is a ${built.entryType}, from start until now.`);
  • Line 2 reads the clock before the work starts. performance.now() gives the time in milliseconds, as a number.
  • Line 3 stands for the work itself. In the store, that's sorting 200,000 prices.
  • Line 4 makes a measure named buildIndex. It starts at start. No end is given, so it ends now.
  • Line 6 prints buildIndex is a measure, from start until now.

Both stay in the page

The browser keeps every mark and measure in the page's performance timeline. That's the list of timing entries the browser keeps for one page, and your code can read it. So the same times you see in DevTools can also go to your analytics: the service that collects data from your real visitors.

Real-life analogyThe shop's logbook

Leaf and Kettle keeps a logbook on the counter. When the kettle goes on, someone writes a line: "kettle on, 6:02." When it boils, they write another: "kettle boiling, 6:02 to 6:06." The lines stay until someone tears the pages out. Each evening, the manager reads the book and copies the numbers into the shop's records.

In real life: A line saying "kettle on, 6:02"
In DevTools: performance.mark(): one moment
In real life: A line saying "kettle boiling, 6:02 to 6:06"
In DevTools: performance.measure(): a stretch of time
In real life: The logbook on the counter
In DevTools: The page's performance timeline
In real life: The manager reading it each evening
In DevTools: Your code reading the entries and sending them to analytics
In real life: Tearing out the full pages
In DevTools: performance.clearMarks() and performance.clearMeasures()

And like the logbook's lines, entries pile up. Here are three clicks:

Marks pile up until you clear themPop out in the code editor (opens in a new tab)JavaScript
for (let click = 1; click <= 3; click++) {  performance.mark("Add to cart");} console.log(`The page keeps ${performance.getEntriesByName("Add to cart").length} marks.`);performance.clearMarks("Add to cart");console.log(`After clearMarks, it keeps ${performance.getEntriesByName("Add to cart").length}.`);
  • Lines 1 to 3 mark Add to cart three times, once per click. Marks can share a name.
  • Line 5 asks the page for every entry with that name. It prints The page keeps 3 marks.
  • Line 6 clears every mark with that name.
  • Line 7 prints After clearMarks, it keeps 0.

The course checked this at a bigger size too. After 10,000 measures, the page still held all 10,000 of them. Why it matters: if your code marks every click and never clears, the list keeps growing. Read what you need, send it, then clear it.

Where DevTools draws them

The Performance panel draws marks and measures on its Timings track. A mark shows as one point in time, and a measure as a bar. Select one, and DevTools' reference says the Summary tab shows "its timestamp, total time, self time, and the detail object".

So User Timing already puts your names in the trace. But it puts all of them on one shared row. In a big app, marks from every part of the code land on that one Timings row. The next step gives your work rows of its own.

The extensibility API for custom tracks

Chrome's DevTools has a way to give your entries their own rows. Its docs call it the performance extensibility API: a way to extend the Performance panel with your own data. There are two ways in. Both are calls in your page's own code, so there's nothing to install.

Way one: a devtools object in a measure

Every mark and measure can carry a detail: a box for any extra data you like. DevTools looks inside it for one key, devtools. If that holds a track, DevTools draws the measure on that track:

Send a measure to the Store trackPop out in the code editor (opens in a new tab)JavaScript
const start = performance.now();// ... the store sorts its 200,000 prices here ...const built = performance.measure("buildIndex", {  start,  detail: { devtools: { track: "Store", trackGroup: "Leaf and Kettle" } },}); console.log(`${built.name} goes on the ${built.detail.devtools.track} track.`);
  • Lines 1 to 4 are the measure from before, spread over several lines.
  • Line 5 adds the detail. Inside it, devtools holds two names. track names your row: Store. trackGroup names a group that keeps related rows together: Leaf and Kettle.
  • Line 8 prints buildIndex goes on the Store track. The measure keeps its detail, so your code can read it too.

The devtools object can say more about the entry. The docs list three more keys for one:

  • color: one name from a fixed list, such as "primary", "secondary" or "tertiary". Each name stands for one of DevTools' own colors.
  • properties: pairs of words, like ["Prices sorted", "200000"]. The Summary tab shows them when you select the entry.
  • tooltipText: a short line that appears when you hover over the entry.

A mark can carry a devtools object too. With dataType: "marker", it becomes a marker: a line drawn down across all the tracks, at one moment. The docs say it shows "a custom marker in the Timings track". The store marks each click on Add to cart this way.

Way two: console.timeStamp

console.timeStamp() is a console method that Chrome extended for custom tracks. It takes everything as a list of arguments, with no object:

Time a redraw with console.timeStampPop out in the code editor (opens in a new tab)JavaScript
const start = performance.now();// ... the store draws 24 teas here ...console.timeStamp("showTeas(24)", start, performance.now(), "Store", "Leaf and Kettle", "secondary"); const left = performance.getEntriesByName("showTeas(24)").length;console.log(`Entries named showTeas(24) left in the page: ${left}.`);
  • Line 1 reads the clock. Line 2 stands for the work: drawing 24 teas.
  • Line 3 hands over six things, in this order: the entry's name, its start, its end, the track, the track group and the color.
  • Line 5 asks the page for entries named showTeas(24).
  • Line 6 prints Entries named showTeas(24) left in the page: 0.

That zero is the point. console.timeStamp() draws an entry on your track, and that's all. The docs say it "does not add entries to the browser's internal performance timeline." So your code can't read it back, and it can't carry properties or a tooltip.

In return, it's "designed for minimal runtime overhead". Overhead is the extra time the timing itself adds. The docs call it "suitable for instrumenting hot paths and production builds." To instrument code means to add timing calls like these to it. A hot path is code that runs very often, like a function called once for every row of a long list.

Real-life analogyA line in the book, or a tick on the board

At Leaf and Kettle, there are two ways to note a kettle boiling. You can write a full line in the logbook: the time, which kettle, how many litres. It takes a moment, and the line stays for the manager. Or, in the busiest hour, you can put a quick tick on the timetable on the wall. It takes a second, and it leaves nothing in the book.

In real life: A full line in the logbook: the time, the kettle, the litres
In DevTools: performance.measure() with properties and a tooltip
In real life: The line stays in the book for the manager
In DevTools: The measure stays in the page's performance timeline
In real life: A quick tick on the timetable on the wall
In DevTools: console.timeStamp(): an entry drawn in the Performance panel only
In real life: Ticks during the busiest hour
In DevTools: Timing a hot path, or code in production

What each call costs

"Minimal overhead" is a claim, so the course timed it. A page made 10,000 calls of each kind, with no recording running, five times over. These are the middle results:

10,000 calls of eachPop out in the code editor (opens in a new tab)JavaScript
// 10,000 calls of each, timed in the page with no recording running, in ms.const measureMs = 16;const timeStampMs = 3.9; const perCall = (ms) => ((ms / 10000) * 1000).toFixed(2);console.log(`performance.measure: ${perCall(measureMs)} µs a call.`);console.log(`console.timeStamp: ${perCall(timeStampMs)} µs a call.`);
  • Lines 2 and 3 hold the recorded totals for 10,000 calls: 16 ms for the measures, 3.9 ms for the time stamps.
  • Line 5 turns a total into the time for one call. It divides by 10,000, then multiplies by 1,000 to get microseconds (µs), millionths of a second.
  • Line 6 prints performance.measure: 1.60 µs a call.
  • Line 7 prints console.timeStamp: 0.39 µs a call.

Both are tiny for one call. The store's buildIndex() runs once per load and once per click, so a measure there costs nothing you could notice. The difference shows up on a hot path. 10,000 measures took 16 ms, about as long as one frame lasts on a screen that shows 60 frames a second. The same 10,000 time stamps took about a quarter of that.

Recording adds to both. While a trace was recording, the same 10,000 calls took 47 ms as measures and 18.5 ms as time stamps. Time them in your own browser:

Playground: what a call costs
The two buttons, shortenedJavaScript
// Time 10,000 calls, then clean up the measures the first kind leaves behind.const time = (fn) => {  const start = performance.now();  for (let call = 0; call < 10000; call++) fn(call);  return performance.now() - start;};const detail = { devtools: { track: "Playground", trackGroup: "Your page", color: "primary" } }; const measureMs = time((call) => performance.measure(`measure ${call}`, { start: performance.now(), detail }));const kept = performance.getEntriesByType("measure").length;performance.clearMeasures();const timeStampMs = time((call) => console.timeStamp(`stamp ${call}`, performance.now(), performance.now(), "Playground", "Your page"));
10,000 callslive
performance.measurenot yet
Measures left behindnot yet
console.timeStampnot yet
Try it yourself
Your browser

Each button makes 10,000 calls of one API and times them. Afterwards, the measures are cleared, so they don't pile up in this page.

This runs in your browser. Times depend on your device, and on whether DevTools is recording. Press each button a few times.

Here are the two ways side by side. Read each row across: one question, answered for each call.

performance.measure or console.timeStamp?
Questionperformance.measure()console.timeStamp()
Does it draw an entry on your own track?Yes, when its detail holds a devtools object with a track.Yes, when you pass a track name as the fourth argument.
Can the entry carry properties and a tooltip?Yes. They show in the Summary tab.No. It gets a name, a start, an end and a color.
Does it stay in the page afterwards?Yes. Your code can read it until it clears it.No. Only the Performance panel sees it.
Which one costs less?It took about 4 times as long as console.timeStamp.This one. The docs made it for code that runs very often.

The store's timings.js

Now the whole file the course added to the store. It uses one of each, so you can compare them on one track:

timings.jsJavaScript
// Put the store's own work on a custom track in the Performance panel.const original = { buildIndex, showTeas }; window.buildIndex = function () {  const start = performance.now();  const prices = original.buildIndex();  performance.measure("buildIndex", {    start,    detail: { devtools: { track: "Store", trackGroup: "Leaf and Kettle", color: "primary", properties: [["Prices sorted", String(prices.length)]], tooltipText: "Sorts past order prices" } },  });  return prices;}; window.showTeas = function (howMany) {  const start = performance.now();  original.showTeas(howMany);  console.timeStamp(`showTeas(${howMany})`, start, performance.now(), "Store", "Leaf and Kettle", "secondary");}; document.addEventListener("click", (event) => {  if (event.target.id !== "add") return;  performance.mark("Add to cart", { detail: { devtools: { dataType: "marker", color: "tertiary", tooltipText: "A click on Add to cart" } } });}, true); 
  • Line 2 keeps the store's own buildIndex() and showTeas(), so the new versions can still call them.
  • Lines 4 to 12 replace buildIndex() with a version that times it. Line 7 is way one: a measure with a devtools object. It sets a track, a group, a color, one property and a tooltip.
  • Lines 14 to 18 do the same for showTeas(). Line 17 is way two: console.timeStamp(), the cheap call.
  • Lines 20 to 23 listen for clicks. Line 22 marks each click on Add to cart as a marker.

The store's own app.js didn't change at all. timings.js loads right after it and wraps its functions from the outside. That's where the four entries and the marker in the first playground came from.

One last step: DevTools only draws custom tracks when a setting is on. In the Performance panel, open Capture settings and tick Show custom tracks.

Annotating a trace

Your colleague will now see the store's own rows. But the trace still holds 18,896 events. Which part should they read first? You know. You need a way to say so, on the trace itself.

Real-life analogySticky notes on a printed timetable

Before you hand the day's timetable to the new manager, you print it and add sticky notes. One goes on the head cook's 6:10 order: "This is the slow one." You highlight 6:20 to 6:21 and write: "The van blocked the door here." Then you draw an arrow from the oven timer set at 6:00 to the rush at 6:20: "This caused that."

In real life: A sticky note on one bar: "This is the slow one"
In DevTools: A label on one entry
In real life: A highlighted stretch, with a note beside it
In DevTools: A labelled time range
In real life: An arrow drawn from one bar to another
In DevTools: A link between two entries
In real life: The notes stay on when you hand it over
In DevTools: Annotations are saved in the trace file

DevTools calls these notes annotations. Its docs: once you've recorded and analyzed a trace, "you can annotate items (with Gemini), time ranges, and create connections." First, open the Annotations tab in the sidebar on the left of the Performance panel. Then:

  • Label an item: double-click a bar, then type your note.
  • Label a time range: hold Shift and drag from the start of the range to its end, then type your note.
  • Connect two items: double-click the first bar, click the arrow that appears next to it, then click the second bar.

To change a note later, double-click it. To delete one, use the Delete button next to it in the Annotations tab. For a bar on the Main track, DevTools can also suggest a label with Gemini, Google's AI model. To do that, it sends the trace data it needs to Google.

The course's three notes

The course added one note of each kind to its recording of the store. Here they are on the recording's clock:

The saved trace's three annotations: a link from the timer installed at 484 ms to the timer firing at 1484.7 ms; a time range from 1484.7 to 1491 ms labelled "The promotion arrives and shifts the page"; and a label on the click's task at 3159.2 ms, 252.1 ms long: "Add to cart: rebuilds the index and draws 1,800 teas".a linka time rangea labelthe promotionthe click's task0 ms1,750 ms3,500 ms
The three notes on the course's recording, one row each. The arrow joins two moments a second apart. The range is a thin slice of time. The label sits on the click's whole task.
  • The arrow starts at 484 ms, where the store's start-up code set a one-second timer. It ends at 1484.7 ms, where that timer fired. This is the promotion banner from Reading a performance trace.
  • The time range runs from 1484.7 ms to 1491 ms: from the timer firing to the moment the page shifted. Its note says: "The promotion arrives and shifts the page."
  • The label sits on the click's task, which ran for 252.1 ms. It says: "Add to cart: rebuilds the index and draws 1,800 teas."

Why it matters: each note answers a question before your colleague asks it. The arrow says what caused the shift. The range says where to zoom in. The label says which task is slow, and why.

How does the file remember them? The notes sit in the file's metadata: the part that describes the recording, apart from the events themselves. The course wrote its notes there the way the Performance panel saves them:

How the trace file keeps the notesJSON
{  "metadata": {    "modifications": {      "annotations": {        "entryLabels": [{ "entry": "r-13321", "label": "Add to cart: rebuilds the index and draws 1,800 teas" }],        "labelledTimeRanges": [{ "bounds": { "min": 1154000465274, "max": 1154000471538, "range": 6264 }, "label": "The promotion arrives and shifts the page" }],        "linksBetweenEntries": [{ "entryFrom": "r-7391", "entryTo": "r-11052" }]      }    }  }}
  • Lines 1 to 4 lead into the notes: the file's metadata, then its modifications, then its annotations.
  • Line 5 is the label. Its entry, r-13321, says which bar it's stuck on. The r means a raw event, one exactly as the browser wrote it. 13321 is that event's place in the file's list of events, counting from 0.
  • Line 6 is the time range. Its bounds are times in microseconds, the unit a trace uses. The range is 6264 µs long, about 6.3 ms.
  • Line 7 is the arrow: from event r-7391, the timer being set, to event r-11052, the timer firing.

The label and the arrow point at events by their place in the file, not by name or time. So when your colleague opens the same file, each note finds the same bar. The course made each key with DevTools' own code for naming events. Then it read the file back to check that each key found its event.

Download the annotated trace (389 kB). In your Performance panel, click Upload and pick the file. The notes come with it.

Sharing traces safely

The trace is ready. Your rows are in it, and so are your notes. Before you send it, look at what else is in the envelope.

Real-life analogyPosting the marked-up timetable

You post the marked-up timetable to a colleague at another branch. When they open it, they see exactly what you saw, notes included. But you also slipped in the counter camera's photos and the day's delivery list. Before you seal the envelope, you look through every photo. One might show a customer's name on an order.

In real life: The envelope
In DevTools: The saved trace file
In real life: The timetable, sticky notes and all
In DevTools: The events, your custom tracks and your annotations
In real life: Photos from the counter camera
In DevTools: Screenshots of the page
In real life: The day's delivery list
In DevTools: Every address the browser asked for
In real life: A copy of the recipe book, only if you put it in
In DevTools: Resource content and source maps

Saving the trace

In the Performance panel, click Download, then Save trace. DevTools offers four options:

  • Include annotations starts on. DevTools only shows it when the trace has notes.
  • Include resource content starts off. When it's on, the page's HTML, CSS and JavaScript go into the file.
  • Include script source maps starts off. A source map links built, shortened code back to the code you wrote. You can only tick this when resource content is on.
  • Compress with gzip starts on. It has been the default since Chrome 142.

About resource content, the docs warn: "If you consider resource contents to be sensitive information, then you should treat the trace file as sensitive as well."

What the course's file holds

The course's annotated trace is saved the way those starting options save it: notes in, no resource content, no source maps, and gzip on. First, its size:

How big the saved trace isPop out in the code editor (opens in a new tab)JavaScript
// The course's annotated trace, saved without resource content or source maps.const file = { events: 18896, bytes: 4737727, gzipBytes: 389261 }; console.log(`${file.events.toLocaleString("en-US")} events, ${(file.bytes / 1e6).toFixed(1)} MB as plain JSON.`);console.log(`${(file.gzipBytes / 1e6).toFixed(2)} MB with gzip.`);
  • Line 2 holds three recorded facts: the number of events, and the file's size in bytes, plain and with gzip.
  • Line 4 prints 18,896 events, 4.7 MB as plain JSON.
  • Line 5 prints 0.39 MB with gzip.

A few seconds of one small page came to almost 5 MB. Gzip made it about 12 times smaller, so it's quicker to send and to open. Size isn't the only thing to check, though. Here is what the file holds:

The saved trace: 18,896 events, with 5 screenshots of the page and every address it loaded; metadata with the annotations; and, only if you choose them, the files' content and source maps.18,896 events5 screenshotsmetadataannotationsresource contentsource mapsalwaysalways; annotations if you keep themonly if you choose
What goes in the envelope. Everything the recording caught is there, screenshots included. Your notes ride along in the metadata. Your code goes in only if you tick the box for it.
What else the saved trace holdsPop out in the code editor (opens in a new tab)JavaScript
const screenshots = { count: 5, bytes: 73764 };const addresses = ["/", "/app.js", "/favicon.ico", "/hero.svg", "/styles.css", "/timings.js"]; console.log(`${screenshots.count} screenshots of the page, ${Math.round(screenshots.bytes / 1000)} kB.`);console.log(`Every address the browser asked for: ${addresses.join(", ")}.`);
  • Line 1 holds the screenshots: pictures of the page, taken while it recorded.
  • Line 2 holds every address the browser asked for while recording. That includes /favicon.ico, which it asked for on its own.
  • Line 4 prints 5 screenshots of the page, 74 kB.
  • Line 5 prints Every address the browser asked for: /, /app.js, /favicon.ico, /hero.svg, /styles.css, /timings.js.

In the file, each address is kept in full, with the server in front. Here that's http://127.0.0.1 and a port number: the course's own computer. For your site, it's every page, file and request the recording caught.

Your custom tracks travel too. Every name, property and tooltip you put in a devtools object is in the file. So is every word of your notes.

So before you send a trace, read it the way a stranger would:

  1. Record with test data, not a real person's account.
  2. Look at the screenshots. They show whatever was on the screen.
  3. Read the addresses. They name every page and file the recording caught.
  4. Read your notes and your custom details.
  5. Tick resource content only if the person needs your code.

Which one?

Each job below is something you might want from a trace. For each one, ask three questions. Does it need details, or data your code reads later? Does it need to be as cheap as possible? Or is it a message for the person who opens the trace?

Which one?
  • Show which filter an image task used, in the entry's details
  • Send the same timing to your analytics, too
  • Time a function that runs thousands of times a second
  • Leave the timing in production code
  • Tell a colleague which task to look at
  • Show that a timer caused a later layout shift
Try it yourself
0 of 6 correct

Sort each job by the tool that fits it best.

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

Common mix-ups

  • "Custom tracks need a DevTools extension." No. They need a devtools object inside a measure's detail, or a console.timeStamp() call with a track name. There's nothing to install.
  • "Timing my code is free." No. With no recording running, 10,000 measures took 16 ms. And every measure stays in the page until your code clears it.
  • "console.timeStamp and performance.measure do the same job." No. Both draw an entry on your track. Only a measure carries properties and a tooltip, and only a measure stays where your code can read it.
  • "Annotations are lost when you save the trace." No. They're saved in the file's metadata, unless you untick Include annotations.
  • "A trace holds only timings." No. It can hold screenshots of the page, every address the browser asked for, and every word in your custom details and notes.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upThe click, in pieces

This is the first example again, with the click's buildIndex() instead of the load's. What does line 6 print now?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// buildIndex after the click, from the same recording, in milliseconds.
const mainTrack = [32.4, 104]; // its bars in the flame chart
const storeTrack = 138.2; // one entry on the store's own track

const total = mainTrack.reduce((sum, ms) => sum + ms, 0).toFixed(1);
console.log(`The Main track shows ${mainTrack.length} pieces, ${total} ms in all.`);
console.log(`The Store track shows one buildIndex entry, ${storeTrack} ms.`);

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

    Exercise 2 · Warm-upIn kilobytes

    Line 5 of the file-size example now gives the gzipped size in kilobytes. What does it print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // The course's annotated trace, saved without resource content or source maps.
    const file = { events: 18896, bytes: 4737727, gzipBytes: 389261 };
    
    console.log(`${file.events.toLocaleString("en-US")} events, ${(file.bytes / 1e6).toFixed(1)} MB as plain JSON.`);
    console.log(`${Math.round(file.gzipBytes / 1000)} kB with gzip.`);

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

      Exercise 3 · PracticeThe missing key

      This measure shows up on the Timings track, but never on a Store track. Which key is missing from its detail?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const start = performance.now();
      // ... the store sorts its 200,000 prices here ...
      performance.measure("buildIndex", {
        start,
        detail: { track: "Store", trackGroup: "Leaf and Kettle" },
      });

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

        Exercise 4 · PracticeThe click's redraw

        In the first playground, how many milliseconds did showTeas(1800), the redraw after the click, take on the Store track?

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

          Exercise 5 · ChallengeA failed step in red

          Your checkout code fails now and then, and you want those entries to stand out. Which color value do DevTools' extensibility docs offer for that?

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

            Exercise 6 · ChallengeNotes out of the way

            You want to read a colleague's trace without their notes for a moment, but without deleting them. Which checkbox in the Annotations tab does that?

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

              Check your understanding

              For each question, ask what each call leaves behind, and what a saved trace carries.

              Custom tracks quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the last line of the cost example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // 10,000 calls of each, timed in the page with no recording running, in ms.
                const measureMs = 16;
                const timeStampMs = 3.9;
                
                const perCall = (ms) => ((ms / 10000) * 1000).toFixed(2);
                console.log(`performance.measure: ${perCall(measureMs)} µs a call.`);
                console.log(`console.timeStamp: ${perCall(timeStampMs)} µs a call.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the last line of the three-marks example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                for (let click = 1; click <= 3; click++) {
                  performance.mark("Add to cart");
                }
                
                console.log(`The page keeps ${performance.getEntriesByName("Add to cart").length} marks.`);
                performance.clearMarks("Add to cart");
                console.log(`After clearMarks, it keeps ${performance.getEntriesByName("Add to cart").length}.`);

                Choose an answer to see the explanation.

              3. Question 3 of 7Which call leaves no entry in the page's own performance timeline?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why can the Main track show one call of buildIndex as several bars, while the Store track shows one?

                Choose an answer to see the explanation.

              5. Question 5 of 7Where does a performance.mark with dataType "marker" show up?

                Choose an answer to see the explanation.

              6. Question 6 of 7In a saved trace, what does an annotation's entry key, such as r-13321, point at?

                Choose an answer to see the explanation.

              7. Question 7 of 7A colleague asks for a trace of a bug in your logged-in account. What do you check before sending it?

                Choose an answer to see the explanation.

              Key takeaways

              • A trace shows what the browser did. Your own entries add what your code meant.
              • performance.mark() writes down a moment, and performance.measure() a stretch of time. Both stay in the page until you clear them.
              • A devtools object in a measure's detail puts it on your own track, with properties and a tooltip.
              • console.timeStamp() is the cheap way, for hot paths. It leaves nothing in the page.
              • Turn on Show custom tracks to see your rows.
              • Annotations label a bar, a time range, or a link from cause to effect. They're saved in the trace.
              • A saved trace carries screenshots, addresses, your details and your notes. Your code goes in only if you choose it.

              Remember the one-liner.
              Name your work in the trace, note what you found, and read what you share.

              Sources

              The facts on this page come from these docs and source files:

              Coming next: Performance work with AI agents. You'll let AI assistance and coding agents record and read traces, and check every claim they make against the trace.

              CompleteFrontend Clear concepts. Working examples.