completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Reading a performance trace

Tracks, flame charts, and summary views: find where the time went in any recording. A real trace of the course's store with a third-party chat widget, read track by track, table by table.

By the end, you can
  • 01
    Read the tracksFind the files, long tasks, frames and taps in a recording, and say what each part of a bar means.
  • 02
    Weigh self and total timeRead a flame chart from the top down, and pick the table that answers your question: Bottom-up, Call tree or Event log.
  • 03
    Walk back to the causeFollow an effect, like a layout shift, back to the line of code that set it off, and split the time between your code and others'.

Where did the time go?

Asha is on the Leaf and Kettle page again, on her phone, on the bus. This week the shop added a small "Chat with us" button. It comes from a chat company. Its code loads from that company's server. Asha writes to the shop: "Your page got slower".

In The Performance panel you learned to record. So you record two things: the page loading, and one tap on Add to cart. The panel fills with rows of coloured bars. Each bar is something the browser did. Where do you even start?

Start with what Asha felt. Her first question is simple: why did it take so long to see anything? The course recorded the store in Chrome, on a phone-sized screen. The CPU is the chip that runs the code. The course made it 4 times slower, so a laptop behaves more like a phone. Here are two times from that recording:

Everything arrived, and still nothing showedPop out in the code editor (opens in a new tab)JavaScript
// The store's load. Times are in ms after the load started.const lastFileArrived = 37.9;const firstPaint = 405.8; console.log(`Every file the page needs had arrived by ${lastFileArrived} ms.`);console.log(`The first text or picture appeared at ${firstPaint} ms.`);console.log(`That leaves ${(firstPaint - lastFileArrived).toFixed(1)} ms to explain.`);
  • Line 1 is a comment. Times are in milliseconds (ms), thousandths of a second, counted from the start of the load.
  • Line 2 is when the last file the page needs finished downloading. The page is its HTML plus four files. They are styles.css, app.js, the chat widget's widget.js, and the picture hero.svg.
  • Line 3 is the first paint: the moment the first text or picture appeared on the screen. Its full name is First Contentful Paint, or FCP.
  • Line 5 prints Every file the page needs had arrived by 37.9 ms.
  • Line 6 prints The first text or picture appeared at 405.8 ms.
  • Line 7 subtracts one from the other. It prints That leaves 367.9 ms to explain.

So the network wasn't the problem. Everything had arrived in under 40 ms. Then, for 367.9 ms, the screen showed no text and no pictures. Something kept the browser from drawing.

To find out what, you read the recording. A recording is called a trace. It's a long list of events. Each event is a thing the browser did, with when it started and how long it took. Here is the whole load in one picture. It shows three of the files and the first paint. It also shows the main thread, which does the page's work.

The store's load on one clock. app.js arrived at 33 ms and waited for the main thread until 284.9 ms; widget.js arrived at 37.9 ms and waited for the main thread until 287.4 ms; hero.svg arrived at 33.5 ms and waited for the main thread until 279.7 ms. The main thread ran four long tasks: hero.svg at 34.9 ms for 245.9 ms, paint at 289 ms for 99 ms, start-up at 389 ms for 144.2 ms, widget at 548.1 ms for 108.2 ms. The first text or picture appeared at 405.8 ms.app.jswidget.jshero.svgmain threadhero.svgpaintstart-upwidgetfirst paint, 405.8 ms0 ms
Time runs left to right. The files had all arrived by 37.9 ms, then waited (the thin lines) while the main thread read hero.svg. The first paint came at 405.8 ms.

Even before you know every part of it, you can see where to look. The files arrive early, and then everything waits for the main thread.

Real-life analogyA detective works backwards

A customer at a café waited twenty minutes for a sandwich. A good detective doesn't start at opening time and read every minute of the day. She starts at the moment the plate reached the table. She asks what the kitchen was doing just before. Then she asks what held that up. Step by step, she walks back to the cause.

In real life: The moment the plate reached the table
In DevTools: The moment the user felt it: the first paint, a slow tap, a jump on the screen
In real life: What the kitchen was doing just before
In DevTools: The task on the main thread at that moment
In real life: What held that dish up
In DevTools: The functions inside the task, and the event that set them off
In real life: The cause, found at last
In DevTools: A line of code, a file or a picture you can change

That's how you read a trace. Each section of this lesson adds one tool for walking back.

  1. The tracks show what each part of the browser was doing at that moment.
  2. The flame chart shows what ran inside one task: which function called which.
  3. The tables add the time up, so you can see which function cost the most.
  4. The arrows lead from an event back to the code that set it off, even a second earlier.
  5. The 1st / 3rd party split tells you whose code it was: the shop's, or the chat company's.
The main idea

Start from the moment that hurt, and walk backwards. The tracks tell you when. The flame chart tells you what called what. The tables tell you how much.

Some of what you'll read comes from JavaScript samples. To take them, Chrome checks over and over which function is running, and writes it down. The JavaScript lesson CPU profiling in depth shows how those samples are taken. This lesson reads them inside a page's trace.

The Network, Frames, Main, and Interactions tracks

When a recording opens, the panel draws it as rows stacked one above the other. Each row is a track: what one part of the browser did, and when. Every track shares the same clock.

Real-life analogyFour cameras, one clock

Leaf and Kettle has four cameras. One watches the back door, where deliveries come in. One watches the kitchen. One watches the counter, where finished plates go out. One watches the customers. In the back office, a single screen plays all four side by side, under one clock. To see why a customer waited at 6:10, you look at all four at 6:10.

In real life: The back-door camera: deliveries coming in
In DevTools: The Network track: each file the page downloads
In real life: The kitchen camera: the head cook at work
In DevTools: The Main track: the main thread's tasks
In real life: The counter camera: plates going out
In DevTools: The Frames track: each frame drawn on the screen
In real life: The customer camera: someone waving for help
In DevTools: The Interactions track: each tap, click or key press
In real life: One clock under all four
In DevTools: One timeline: read straight down to see what happened at the same moment

The Main track: one task at a time

Start with the Main track. It shows the main thread, the head cook from Processes and threads. It reads the HTML, works out styles and layout, paints, and runs your JavaScript. It does one job at a time. Each job, from start to finish, is a task, drawn as one bar.

A long task is a task that runs for more than 50 ms. While it runs, the main thread can't start anything else. A tap has to wait, and so does the next update to the screen. DevTools marks each long task with a red triangle. It also shades the part over 50 ms in red. The store's load had 4 long tasks.

The load's four long tasksPop out in the code editor (opens in a new tab)JavaScript
// The load's long tasks: when each started, and how long it ran, in ms.const longTasks = [  { start: 34.9, ms: 245.9, inside: "the picture, hero.svg" },  { start: 289, ms: 99, inside: "the page's layout and paint" },  { start: 389, ms: 144.2, inside: "the store's start-up code" },  { start: 548.1, ms: 108.2, inside: "the chat widget's script" },]; for (const task of longTasks) console.log(`At ${task.start} ms: ${task.ms} ms on ${task.inside}.`);
  • Lines 3 to 6 are the four long tasks. Each says when it started, how long it ran, and what the trace says was inside.
  • Line 9 prints one sentence per task. The first two are At 34.9 ms: 245.9 ms on the picture, hero.svg. and At 289 ms: 99 ms on the page's layout and paint.
  • The last two are At 389 ms: 144.2 ms on the store's start-up code. and At 548.1 ms: 108.2 ms on the chat widget's script.

Three are easy to name.

  • The second is the browser working out where everything goes on the page, then painting it.
  • The third is the store's own start-up code in app.js. It runs on DOMContentLoaded, an event the browser fires once it has read all of the HTML.
  • The fourth is the chat widget's script running.

The first one is the surprise. It's a picture. Why would a picture keep the main thread busy for a quarter of a second?

Most pictures don't. But hero.svg is an SVG: a picture written as text, as a list of shapes. Chrome draws it by building a small document of its own, much like a web page. Chromium is the project Chrome is built from. Its code says: "Internally, SVGImage creates a IsolatedSVGDocumentHost and reuses the existing paint code in Blink to draw the image". Blink is the part of Chrome that turns pages into pixels.

The store's hero.svg is 580 KB of text, with 14,000 dots in it. Reading it took 162.7 ms. Next came a style recalculation of 14,029 elements, which took 53.5 ms. Then layout took 24.6 ms. All of it ran on the main thread, in one long task. So an SVG doesn't only cost download time. Every shape in it is work for the head cook.

Now the red parts. Here is how far each long task ran past 50 ms:

The part of each task shaded redPop out in the code editor (opens in a new tab)JavaScript
// How long each long task ran, in ms. DevTools shades the part over 50 ms red.const longTasks = [245.9, 99, 144.2, 108.2];const redParts = longTasks.map((ms) => ms - 50); console.log(`The red parts: ${redParts.map((ms) => ms.toFixed(1)).join(", ")} ms.`);console.log(`In all, ${redParts.reduce((sum, ms) => sum + ms, 0).toFixed(1)} ms over the limit.`);
  • Line 2 holds how long each long task ran.
  • Line 3 takes 50 ms off each one. What's left is the part DevTools shades red.
  • Line 5 prints The red parts: 195.9, 49.0, 94.2, 58.2 ms.
  • Line 6 adds them up. It prints In all, 397.3 ms over the limit.

Nearly 400 ms of red, in a load that took well under a second. A long task with a big red part is the first place to look. Nothing else can happen on the main thread while it runs.

The Network track: arrived isn't the same as used

The Network track draws each request as a bar. Chrome's DevTools docs split each bar into four parts, from left to right.

  • A thin line on the left: everything before the request is sent, such as opening a connection.
  • A light part: sending the request and waiting for the server to answer.
  • A dark part: downloading the file.
  • A thin line on the right. In the docs' words, it's "the time spent waiting for the main thread".

That last line answers part of Asha's question. The files arrived early, then waited for the head cook to be free.

Downloaded, then waitingPop out in the code editor (opens in a new tab)JavaScript
// When each file's last byte arrived, and when the main thread got to it, in ms.const files = {  "app.js": { arrived: 33, handled: 284.9 },  "widget.js": { arrived: 37.9, handled: 287.4 },  "hero.svg": { arrived: 33.5, handled: 279.7 },}; for (const [file, { arrived, handled }] of Object.entries(files)) {  console.log(`${file} arrived at ${arrived} ms, then waited ${(handled - arrived).toFixed(1)} ms.`);}
  • Lines 3 to 5 hold three files. For each, they say when its last byte arrived, and when the main thread got to it.
  • Line 9 subtracts the two times for each file, and prints a sentence.
  • It prints app.js arrived at 33 ms, then waited 251.9 ms. and widget.js arrived at 37.9 ms, then waited 249.5 ms.
  • Then it prints hero.svg arrived at 33.5 ms, then waited 246.2 ms.

All three waited about a quarter of a second. Now look at when the waits end. Each one ends within 7 ms of 280.8 ms, the moment the hero.svg task finished. The head cook was reading the picture, and everything else queued behind it.

Why it matters: a long thin line on the right of a Network bar isn't a slow server. A faster server won't shorten it. Freeing the main thread will.

The Frames and Interactions tracks: what Asha saw and did

The Frames track shows each frame: one picture of the page on the screen. The docs name four kinds:

  • An idle frame, in white: nothing changed.
  • A frame, in green: drawn in time.
  • A partially presented frame, in yellow: Chrome got only some of the changes on screen in time.
  • A dropped frame, in red: Chrome couldn't draw it in time.

Chrome drew 4 frames in the whole load recording. Tick Screenshots before you record, and you can see each one.

The Interactions track shows each tap, click or key press. An interaction has three parts:

  • Input delay: from the tap until the page's code starts to run.
  • Processing: the page's event handlers running.
  • Presentation delay: from the end of the handlers until the next frame is on the screen.

The track draws processing as a bar. The two delays are thin lines on either side. The docs call those lines "whiskers". An interaction over 200 milliseconds gets a red triangle in its corner. 200 ms is also the limit for a good Interaction to Next Paint (INP). INP is the Core Web Vital for how fast a page answers. Here is Asha's tap:

One tap, in three partsPop out in the code editor (opens in a new tab)JavaScript
// The tap on Add to cart, in three parts, in ms.const click = { inputDelay: 2.5, processing: 157.9, presentationDelay: 137.7 };const total = click.inputDelay + click.processing + click.presentationDelay; console.log(`The click took ${total.toFixed(1)} ms, from the tap to the next frame.`);console.log(`Its event handlers ran for ${click.processing} ms.`);console.log(`The last ${click.presentationDelay} ms came after they finished.`);
  • Line 2 holds the three parts of the tap on Add to cart, from the click recording.
  • Line 3 adds them up.
  • Line 5 prints The click took 298.1 ms, from the tap to the next frame.
  • Line 6 prints Its event handlers ran for 157.9 ms.
  • Line 7 prints The last 137.7 ms came after they finished.

The tap is well over 200 ms. So it gets a red triangle. And nearly half of it came after the handlers had finished. That was the browser getting the next frame onto the screen. So a faster handler would help, but only with part of the wait.

Try both recordings. In the load, click a file's bar. You'll see when it arrived and when the main thread got to it. Or click a long task to see what was inside. Then switch to the click, and select the interaction.

Playground: the tracks of two recordings
Network and Main0 to 700 ms
Network
Main
long tasks
markersFCPDCLL
Summaryrecorded

Select something in the tracks.

Try it yourself
Summary

The store loading, with the CPU 4 times slower. Click a file's bar to see when it arrived, or a long task to see what was inside.

The course recorded these in Chrome, on a phone-sized screen with the CPU 4 times slower, and kept the middle one of five loads. Every bar comes from the trace's events and JavaScript samples.

Flame charts, self time, and total time

The Interactions track told you the tap's handlers ran for 157.9 ms. But which code? Zoom into the Main track at that moment, and the task opens up. Under it, you see what ran inside. Under each of those, you see what they ran. This view is called a flame chart.

Real-life analogyOne order, step inside step

The head cook gets an order for mint tea. Afterwards, she draws it as a chart, with time running left to right. On the top row: "mint tea", as wide as the whole order took. Under it, the steps it needed: "boil water", then "brew". Under "boil water", a smaller step: "fill the kettle". Where the top row has nothing under it, she was busy with the order itself. She was reading the ticket and finding a cup.

In real life: The whole order, on the top row
In DevTools: The event that started the work, at the top of the flame chart
In real life: A step, written under the job it was part of
In DevTools: A function, drawn under the function that called it
In real life: How wide a row is
In DevTools: How long it ran, including everything under it: its total time
In real life: The part of a row with nothing under it
In DevTools: Time spent in that function's own code: its self time

The docs say it in three short sentences: "The x-axis represents the recording over time. The y-axis represents the call stack. The events on top cause the events below". One at a time:

  • Left to right is time, as on every other track.
  • Top to bottom is the call stack: the chain of functions calling each other. A function sits right under the one that called it.
  • The top row is the root activity: the thing that made the browser start this work. When you click a page, the docs say, the root activity is an Event.

Here is the tap's flame chart, built from the click recording's JavaScript samples:

The click's call tree, drawn like a flame chart. The click event called the click handler (150.7 ms in all), which called buildIndex (134.7 ms) and showTeas (15.8 ms). buildIndex called sort's comparator.Event: clickclick handlerbuildIndexcomparatorshowTeas
Each bar is as wide as its total time. What a function calls sits under it. The part of a bar with nothing under it is its self time.

Read it from the top. The click event ran the store's click handler. The handler called two functions. buildIndex sorts 200,000 old order prices, and showTeas puts the teas on the page. buildIndex called one more. It's the small function it gives sort to compare two prices, called the comparator.

Two times for every bar

Every bar in a flame chart has two times. The docs define them.

  • Total time is "the time spent in that activity or any of its children". It's how wide the bar is.
  • Self time is "the time directly spent in that activity". It's the part of the bar with nothing under it.

The "children" are the functions it called, also called its callees. Here are the two times for buildIndex:

Total time and self timePop out in the code editor (opens in a new tab)JavaScript
// buildIndex, from the click's flame chart, in ms.const buildIndex = { total: 134.7, self: 118.7 }; console.log(`buildIndex ran for ${buildIndex.total} ms in all.`);console.log(`${buildIndex.self} ms of that was its own code.`);console.log(`${(buildIndex.total - buildIndex.self).toFixed(1)} ms was in functions it called.`);
  • Line 2 holds buildIndex's two times from the click.
  • Line 4 prints buildIndex ran for 134.7 ms in all.
  • Line 5 prints 118.7 ms of that was its own code.
  • Line 6 takes the self time away from the total. It prints 16.0 ms was in functions it called.

That 16.0 ms is the comparator. Its bar is 16 ms wide. It calls nothing, so its self time and its total time are the same.

But most of buildIndex's time is its own. Why? It makes 200,000 numbers and sorts them. The sorting runs inside the JavaScript engine: the part of Chrome that runs JavaScript. In these samples, it has no bar of its own. So it counts as buildIndex's self time.

Now the click handler, one row down from the top:

A wide bar with almost nothing of its ownPop out in the code editor (opens in a new tab)JavaScript
// The store's click handler, from the click's flame chart, in ms.const clickHandler = { total: 150.7, self: 0.1 }; console.log(`The click handler ran for ${clickHandler.total} ms in all.`);console.log(`Only ${clickHandler.self} ms of that was its own code.`);
  • Line 2 holds the click handler's two times.
  • Line 4 prints The click handler ran for 150.7 ms in all.
  • Line 5 prints Only 0.1 ms of that was its own code.

The handler's bar is as wide as the click event above it. Yet almost none of it is its own. It started the work and waited while its callees did it. There's almost nothing to fix in the handler itself.

Why it matters: read a flame chart from the top down. Find a wide bar, then follow it down to the bars with the most self time. That's where the work really happened. In the tap, it's buildIndex. In the first playground, switch to the click and select each bar to see both of its times.

Bottom-up, call tree, and event log

A flame chart is good for one task. But a whole load has many tasks. The same function can run in several of them. To add it all up, the panel has three tables under the tracks. Each is in its own tab: Bottom-up, Call tree and Event log. Each one only shows the part of the recording you've selected.

Real-life analogyOne till receipt, read three ways

After a big shop, you look at your till receipt. As printed, it lists every item in the order it was scanned. You can also add up each item. Three bottles of milk become one line, and you see what cost the most. Or you can add it up by aisle. Then dairy gets one subtotal and bakery another, each covering everything from that aisle.

In real life: The receipt as printed, in the order things were scanned
In DevTools: The Event log: activities in the order they happened
In real life: Each item added up, however many times it was scanned
In DevTools: Bottom-up: each activity's self time, added up across every time it ran
In real life: Each aisle's subtotal, covering everything from that aisle
In DevTools: The Call tree: each root activity, with the total time of everything under it

Read this table across: each row is one tab, the question it answers, and how it lays out the time.

Three tables, three questions
The tabThe question it answersHow it shows the time
Bottom-upWhere was the time spent directly?One row per activity. Its self time is added up across every time it ran.
Call treeWhat started the work?Root activities at the top. What each one called is nested under it.
Event logIn what order did things happen?Root activities in the order they started, each with its start time.

Bottom-up and the Call tree can put different functions first. That isn't a mistake. They look at different times. Here are two rows from the load: the store's start-up code and the function it calls.

Same rows, two different winnersPop out in the code editor (opens in a new tab)JavaScript
// Two rows from the load's samples, in ms.const rows = [  { name: "the DOMContentLoaded listener", total: 136.2, self: 0.2 },  { name: "buildIndex", total: 134.1, self: 113.2 },]; const mostTotal = [...rows].sort((a, b) => b.total - a.total)[0];const mostSelf = [...rows].sort((a, b) => b.self - a.self)[0];console.log(`Most total time: ${mostTotal.name}.`);console.log(`Most self time: ${mostSelf.name}.`);
  • Lines 3 and 4 hold the two rows. One is the DOMContentLoaded listener. The other is buildIndex, which it calls.
  • Line 7 sorts the rows by total time and keeps the first. Line 8 does the same by self time.
  • Line 9 prints Most total time: the DOMContentLoaded listener.
  • Line 10 prints Most self time: buildIndex.

The listener started the work, so its total time is the biggest. The Call tree starts from what began the work. So the listener is near its top, with buildIndex nested under it. buildIndex did the work, so its self time is the biggest. That puts it at the top of Bottom-up.

Why it matters: ask "what started this?" and open the Call tree. Ask "where did the time really go?" and open Bottom-up. Ask "what happened just before?" and open the Event log.

Try all three on the whole load. In Bottom-up, see how high up the chat widget's code comes. Then open the Call tree and find widget.js. Its rows have big total times but almost no self time. The work happens further down, in what they call.

Playground: Bottom-up, Call tree and Event log
Self timeTotal timeActivity
113.2 ms134.1 msbuildIndex app.js:1824
106.1 ms106.1 mschecksum widget.js:3
19.2 ms19.2 ms(anonymous) app.js:1826
2.1 ms2.1 ms(garbage collector) built in
1.7 ms1.7 ms(anonymous) app.js:1825
1.1 ms2.0 msshowTeas app.js:1829
1.1 ms1.1 ms(anonymous) app.js:1
0.9 ms0.9 msdrawButton widget.js:17
Try it yourself
Bottom-up

Each function's own time, added up across the whole load. buildIndex has the most self time: 113.2 ms.

All three tables read the same load. Bottom-up and the Call tree come from the JavaScript samples. The Event log lists the main thread's events of 1 ms or longer.

Following a task from cause to effect

Asha has one more complaint. A second after the page appears, the tea list jumps down. This is called a layout shift. It's something visible moving from one frame to the next, without the user asking for it.

The trace shows the shift at 1539.3 ms. But the shift is only the effect. The detective's question is: what caused it? For that, the Main track can draw arrows.

Real-life analogyA kitchen timer with a note on it

At 6:00, the head cook puts scones in the oven and sets a timer for twenty minutes. She sticks a note on it: "Set by the head cook, for the scones". Then she goes back to other orders. At 6:20 the timer rings. The scones come out. The plates on the counter get pushed along to make room. Later, someone asks why the plates moved. The note takes you back to 6:00, and to who set the timer.

In real life: Setting the timer, with a note saying who set it
In DevTools: Install Timer: the line of code that called setTimeout
In real life: The timer ringing, twenty minutes later
In DevTools: Timer Fired: the timer's function runs
In real life: The plates pushed along the counter
In DevTools: A layout, then a layout shift
In real life: Reading the note to get back to 6:00
In DevTools: Following the arrow, or the Summary tab's link, back to what set it off

An event that causes another one later is called an initiator. The Main track "can show arrows that connect the following initiators and the events they caused". Here are three of the pairs on the docs' list.

  • Install Timer to Timer Fired: code calls setTimeout, and later the timer's function runs.
  • Request Idle Callback to Fire Idle Callback: code asks to run something when the browser is free. Later, it runs.
  • A change to styles or layout, to the Recalculate styles or Layout that it caused.

To see an arrow, select either end of it in the flame chart. The Summary tab then links to the other end: "Initiator for" on the cause, and "Initiated by" on the effect. Click the link to jump there. The Network track does the same for files. Click a request, and it draws an arrow from its initiator to the request.

Here is the store's promotion banner, from the load recording.

From cause to effect. Install Timer at 531.8 ms, Timer Fired at 1532.9 ms, Layout at 1534.9 ms, and a Layout Shift at 1539.3 ms.Install Timer531.8 msTimer Fired1532.9 msLayout1534.9 msLayout Shift1539.3 ms
Install Timer and Timer Fired are a whole second apart, yet DevTools joins them with an arrow. The layout and the shift follow within a few milliseconds.
From timer to layout shiftPop out in the code editor (opens in a new tab)JavaScript
// Four events from the load, in the order they happened. Times are in ms.const chain = [  { at: 531.8, event: "Install Timer", detail: "app.js line 1818, for 1000 ms" },  { at: 1532.9, event: "Timer Fired", detail: "its callback ran 0.7 ms" },  { at: 1534.9, event: "Layout", detail: "3.4 ms" },  { at: 1539.3, event: "Layout Shift", detail: "score 0.1369, content moved 120 px down" },]; for (const step of chain) console.log(`${step.at} ms: ${step.event}, ${step.detail}`);console.log(`fired ${(chain[1].at - chain[0].at).toFixed(1)} ms after it was installed`);
  • Lines 3 to 6 are four events from the load, in the order they happened.
  • Line 9 prints each one. The first two are 531.8 ms: Install Timer, app.js line 1818, for 1000 ms and 1532.9 ms: Timer Fired, its callback ran 0.7 ms.
  • The next two are 1534.9 ms: Layout, 3.4 ms and 1539.3 ms: Layout Shift, score 0.1369, content moved 120 px down.
  • Line 10 takes the install time from the fired time, and prints fired 1001.1 ms after it was installed.

Now walk it backwards, the way the detective would:

  1. At 1539.3 ms, the layout shift moved the content 120 px down.
  2. Just before it, at 1534.9 ms, a 3.4 ms layout worked out where everything should go.
  3. Just before that, at 1532.9 ms, a timer fired. Its function ran for only 0.7 ms. It put a promotion banner into the page, above the teas.
  4. The arrow from Timer Fired leads back a whole second, to 531.8 ms. That's app.js line 1818, inside the store's start-up code, calling setTimeout with 1000 ms.

Without the arrow, you'd see a 0.7 ms function and a 3.4 ms layout, far from anything slow. Neither looks guilty. The arrow joins them to the line that set it all up.

Why it matters: that one shift scored 0.1369. web.dev asks sites to keep Cumulative Layout Shift (CLS) at 0.1 or less. CLS is the Core Web Vital for things jumping around. This single shift was enough to miss it. The fix belongs at line 1818: keep space for the banner before it arrives.

Third-party and first-party breakdowns

Back to the chat widget. Code that you write and serve yourself is first-party. Code from another company, loaded from its server, is third-party. Chat widgets, ads, analytics and video players are all third-party. It's like a guest cook in your kitchen. The dish is theirs, but they use your stove. Your orders wait while they cook.

The panel has two tools for telling them apart.

  • Dim 3rd parties is a checkbox in the action bar at the top. Tick it, and the panel "grays out third-party-related events from the performance trace and leaves only the first-party highlighted".
  • The 1st / 3rd party table is in the Summary tab. Select a stretch of the recording, with no single event selected. The table lists each company with its transfer size, the bytes it downloaded, and its main-thread time.

The course split the load's JavaScript time the same way, by where the code came from. In the recording, the store was served from 127.0.0.1 and the widget from localhost: two different origins. An origin is the scheme, host and port of an address.

Whose JavaScript was it?Pop out in the code editor (opens in a new tab)JavaScript
// The load's JavaScript time on the main thread, by where the code came from, in ms.const javascriptMs = { "The store, from 127.0.0.1": 137.9, "The chat widget, from localhost": 107.6 };const total = Object.values(javascriptMs).reduce((sum, ms) => sum + ms, 0); for (const [who, ms] of Object.entries(javascriptMs)) {  console.log(`${who}: ${ms} ms, ${Math.round((ms / total) * 100)}% of all the JavaScript.`);}
  • Line 2 holds the JavaScript time each origin spent on the main thread, from the samples.
  • Line 3 adds the two together.
  • Line 6 prints each one's time, and its share of the whole.
  • It prints The store, from 127.0.0.1: 137.9 ms, 56% of all the JavaScript.
  • Then it prints The chat widget, from localhost: 107.6 ms, 44% of all the JavaScript.

The chat widget is one small button. Yet it ran more than three-quarters as much JavaScript as the whole store. And it ran as one long task, of 108.2 ms.

The widget loads with async. That lets the browser keep reading the HTML while the script downloads. But async only changes when a script runs, not how long. Once it runs, the main thread is busy with it until it finishes.

Why it matters: before you add a third-party script, check its cost in the 1st / 3rd party table. If it's big, ask whether it can load later, for example when someone first opens the chat.

Which view answers it?

Each question below is one you might ask about a trace. Sort it by the table that answers it. Ask yourself: is it about where time went directly, about what started the work, or about order?

Which view answers it?
  • Which function spent the most time in its own code?
  • How much did garbage collection cost, all told?
  • Which root activity caused the most work?
  • What did the DOMContentLoaded listener call, and how long did each take?
  • What ran just before the layout shift?
  • Did widget.js run before or after the DOMContentLoaded listener?
Try it yourself
0 of 6 correct

Sort each question by the table that answers it.

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

Common mix-ups

  • "Total time is what a function cost by itself." No. Total time includes everything it called. In the tap, buildIndex ran 134.7 ms in all, and 118.7 ms of that was its own.
  • "A long Network bar means a slow server." No. The thin line on the right is time spent waiting for the main thread. app.js arrived at 33 ms, but its bar ran on to 284.9 ms.
  • "Pictures only cost download time." No. Reading hero.svg took 162.7 ms of the main thread, and its style and layout came after that.
  • "An async third-party script can't slow the page down." No. async changes when a script runs, not how long. The widget ran as a 108.2 ms long task.
  • "The top of a flame chart is the slowest part." No. The top is what started the work. Look lower down, at the bars with the most self time.
  • "An interaction's cost is all in its handler." No. The tap took 298.1 ms, and 137.7 ms of that came after the handlers had finished.
  • "The cause of a problem sits right next to it in the trace." No. The timer that moved the content was set a whole second earlier, at 531.8 ms.

Practice exercises

Try each one before you open the hints.

Exercise 1 · Warm-upFrom the ring to the shift

The last line now measures from the timer firing to the layout shift. What does it print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// Four events from the load, in the order they happened. Times are in ms.
const chain = [
  { at: 531.8, event: "Install Timer", detail: "app.js line 1818, for 1000 ms" },
  { at: 1532.9, event: "Timer Fired", detail: "its callback ran 0.7 ms" },
  { at: 1534.9, event: "Layout", detail: "3.4 ms" },
  { at: 1539.3, event: "Layout Shift", detail: "score 0.1369, content moved 120 px down" },
];

for (const step of chain) console.log(`${step.at} ms: ${step.event}, ${step.detail}`);
console.log(`shifted ${(chain[3].at - chain[1].at).toFixed(1)} ms after it fired`);

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

    Exercise 2 · Warm-upOne decimal place

    The shares now keep one decimal place. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // The load's JavaScript time on the main thread, by where the code came from, in ms.
    const javascriptMs = { "The store, from 127.0.0.1": 137.9, "The chat widget, from localhost": 107.6 };
    const total = Object.values(javascriptMs).reduce((sum, ms) => sum + ms, 0);
    
    for (const [who, ms] of Object.entries(javascriptMs)) {
      console.log(`${who}: ${ms} ms, ${((ms / total) * 100).toFixed(1)}% of all the JavaScript.`);
    }

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

      Exercise 3 · PracticeSecond in Bottom-up

      In the playground's Bottom-up table, which function comes second, with the second most self time in the load?

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

        Exercise 4 · PracticeLow priority

        In the playground, which request had Low priority?

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

          Exercise 5 · ChallengeFrom the party table to a table

          In the Summary tab's 1st / 3rd party table, you want to see one company's activities in a table. What do you click next to it?

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

            Exercise 6 · ChallengeWhere the arrow points

            Your code calls requestAnimationFrame. In the Main track, you select the Request Animation Frame event. Which event does its arrow point to?

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

              Check your understanding

              For each question, ask which track or table shows it, and whether it's about self time, total time, or order.

              Reading a performance trace quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the last line of the buildIndex example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // buildIndex, from the click's flame chart, in ms.
                const buildIndex = { total: 134.7, self: 118.7 };
                
                console.log(`buildIndex ran for ${buildIndex.total} ms in all.`);
                console.log(`${buildIndex.self} ms of that was its own code.`);
                console.log(`${(buildIndex.total - buildIndex.self).toFixed(1)} ms was in functions it called.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7A bar in the flame chart is wide, but has almost no self time. Where did its time go?

                Choose an answer to see the explanation.

              3. Question 3 of 7When should you open the Call tree tab?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the last line of the cause and effect example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // Four events from the load, in the order they happened. Times are in ms.
                const chain = [
                  { at: 531.8, event: "Install Timer", detail: "app.js line 1818, for 1000 ms" },
                  { at: 1532.9, event: "Timer Fired", detail: "its callback ran 0.7 ms" },
                  { at: 1534.9, event: "Layout", detail: "3.4 ms" },
                  { at: 1539.3, event: "Layout Shift", detail: "score 0.1369, content moved 120 px down" },
                ];
                
                for (const step of chain) console.log(`${step.at} ms: ${step.event}, ${step.detail}`);
                console.log(`fired ${(chain[1].at - chain[0].at).toFixed(1)} ms after it was installed`);

                Choose an answer to see the explanation.

              5. Question 5 of 7In the Interactions track, what does a red triangle in an interaction's corner mean?

                Choose an answer to see the explanation.

              6. Question 6 of 7In the recording, why did app.js's Network bar run on long after its last byte arrived?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which tab lists activities in the order they happened?

                Choose an answer to see the explanation.

              Key takeaways

              • Start from the moment that hurt, such as the first paint, a slow tap or a jump, and walk backwards.
              • Every track shares one clock. Read straight down to see what happened at the same moment.
              • A long task keeps the main thread busy for more than 50 ms. Nothing else runs on it until the task ends.
              • The thin line on the right of a Network bar is waiting for the main thread, not for the server.
              • In a flame chart, a function sits under the one that called it. Total time is the bar's width. Self time is the part with nothing under it.
              • Bottom-up finds where time was spent directly. The Call tree finds what started the work. The Event log keeps the order.
              • Initiator arrows lead from an effect back to its cause, even a second earlier.
              • Dim 3rd parties and the 1st / 3rd party table split the time between your code and other companies'.

              Remember the one-liner.
              Walk back from the moment that hurt: tracks for when, flame charts for what called what, tables for how much.

              Sources

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

              Coming next: Performance insights. The panel can read a trace for you and point at likely causes. You'll check each one with the tools from this lesson.

              CompleteFrontend Clear concepts. Working examples.