Performance insights
Use the panel's insights to go from a trace to a likely cause, and check each one yourself. Ten insights on a real recording of the course's store, worked out by DevTools' own trace engine, and one it got wrong.
- 01Read the insightsSay what each insight checks, from the LCP breakdown to 3rd parties, and what fail, informative and pass mean.
- 02Find the evidenceGo from an insight to the requests, tasks and layout shifts in the trace that it points at.
- 03Check the answerTreat insights, estimated savings and AI answers as leads. Confirm them in the trace, then in a new recording.
From a trace to a likely cause
Asha told Leaf and Kettle that its page is slow on her phone. In the last two lessons, you recorded the store on a slow phone and read the recording by hand. But a recording is huge. The one in this lesson holds 59,581 events. Where do you even start?
You don't start from nothing. When a recording stops, the Performance panel reads it for you. In its left sidebar, the Insights tab lists what it found. Each item is an insight: a check DevTools runs over the recording, looking for one kind of problem. A slow server is one kind. A photo sent far too big is another. Lighthouse, the report tool built into DevTools, shows the same insights.
Here is the store's recording in the panel, with the Insights tab on the left:
Instead of 59,581 events, you get a list of twelve things to check.
Insights fill the gap the numbers leave. The live view from The Performance panel told you that the store is slow. An insight tells you what might be making it slow. It's the difference between a doctor saying "your blood pressure is high" and "here's what's pushing it up".
A slower store to practice on
To give the insights plenty to find, the course made the store from Reading a performance trace slower on purpose. It kept the chat widget, and changed four things:
- The main picture is now a real photo of mint tea. Its
<img>tag has no width or height. - The headings use a web font: a font file the page downloads, instead of one already on the phone.
- A new script,
reviews.js, adds 250 reviews after the page has loaded. - The server waits 700 ms before it sends the HTML.
Then the course recorded the store the way Asha meets it: on a phone-sized screen, with DevTools' Slow 4G network setting and a CPU 4 times slower. The recording holds the page load, three quiet seconds, and one click on Add to cart.
Last, the course ran DevTools' own trace engine on the saved recording. That's the code inside the Performance panel that works out insights. So every insight below is one the panel shows.
Warning lights, not answers
You're driving, and an orange light comes on: an oil can. The light doesn't fix anything. It doesn't even prove the oil is low. It tells you which part to check. So you stop, open the bonnet, and pull out the dipstick. The oil may really be low. Or the sensor may be wrong.
- In real life: One warning light on the dashboard
- In DevTools: One insight in the Insights tab
- In real life: The part the light points at: the oil
- In DevTools: The requests, tasks or layout shifts the insight names
- In real life: Opening the bonnet and checking the dipstick
- In DevTools: Looking at those events in the trace
- In real life: A faulty sensor
- In DevTools: An insight that blames the wrong thing
Every insight names its evidence: the requests, tasks or layout shifts it found. Hover over one in the Insights tab, and the panel highlights them in the trace. That's your bonnet. Look at the events, and decide whether the insight is right.
Each insight ends in one of three states:
- Fail: the check found something to fix. Many failed insights come with an estimated saving: how much sooner a metric could be if you fixed it.
- Informative: the check has something to show you, without calling it a problem.
- Pass: the check found nothing. The panel folds passed insights into a closed Passed insights section at the bottom.
On the store's recording, the engine ran 19 insights. Seven passed. Ten failed, and two were informative. The playground below holds eleven of those twelve. The twelfth, Network dependency tree, isn't covered here.
The playground lists the insights in the same order as the panel. Pick one to see its numbers, and where to look for them in the trace:
702 msnonefineNeeds a look. To check it: Select the HTML's request in the Network track. The light part of its bar is the wait for the server.
An insight is a lead, not a verdict. It points at a likely cause. You confirm the cause in the trace, change one thing, and record again. If the metric gets better, you found a real cause.
Here are those four steps as a picture. Every section below follows them.
LCP and INP breakdowns
Asha's first complaint was the photo: it took ages to appear. That moment has a name: Largest Contentful Paint, or LCP. It's when the biggest picture or block of text on the screen appeared. On this recording, LCP was 2,819.5 ms. The insight's docs say to aim for 2.5 seconds or less. So the LCP breakdown insight failed.
A breakdown doesn't give you a new number. It splits the one you have into parts, so you can see which part was too long.
Asha orders mint tea at Leaf and Kettle, and waits ten minutes. Where did they go? First, she waited for someone to take her order. Then the order sat on the counter before anyone started. Then the tea brewed. Then the cup waited to be carried to her table. Only the brewing was making tea. The rest was waiting.
- In real life: Waiting for someone to take your order
- In DevTools: Time to first byte: waiting for the HTML to start arriving
- In real life: The order sitting on the counter
- In DevTools: Load delay: the time before the photo starts downloading
- In real life: The tea brewing
- In DevTools: Load duration: the photo downloading
- In real life: The cup waiting to be carried to your table
- In DevTools: Render delay: the photo, downloaded, waiting to be drawn
Here are the four parts of LCP, as the insight's docs define them:
- Time to first byte (TTFB): from the moment the visit starts until the first byte of the HTML arrives.
- Load delay: from TTFB until the browser starts loading the photo.
- Load duration: how long the photo takes to load.
- Render delay: from the end of the download until the photo is fully drawn.
This example prints the store's four parts, and each part's share of the whole:
// The LCP breakdown for the photo, in milliseconds.const parts = { "Time to first byte": 703.2, "Load delay": 32, "Load duration": 1734.4, "Render delay": 349.9 };const lcp = Object.values(parts).reduce((sum, ms) => sum + ms, 0); for (const [part, ms] of Object.entries(parts)) { console.log(`${part}: ${ms} ms, ${Math.round((ms / lcp) * 100)}% of LCP`);}console.log(`LCP: ${lcp.toFixed(1)} ms`);- Line 2 holds the four parts the insight recorded, in milliseconds.
- Line 3 adds them up, which gives LCP.
- Lines 5 to 7 print each part with its share of LCP. First come
Time to first byte: 703.2 ms, 25% of LCPandLoad delay: 32 ms, 1% of LCP. Then comeLoad duration: 1734.4 ms, 62% of LCPandRender delay: 349.9 ms, 12% of LCP. - Line 8 prints
LCP: 2819.5 ms.
The insight's description gives you a yardstick: "Ideally, most of the LCP time should be spent on loading the resources, not within delays." Here, most of it was: 62% went on the photo's download, and 1% on waiting to start it. So the photo wasn't stuck in a queue. It took a long time to arrive.
Time to first byte, a quarter of LCP, points at the server. The download points at the photo's size. Both have insights of their own, further down.
Could the browser find the photo early?
Next in the list is LCP request discovery. It runs three checks on the photo's request:
- Is the photo in the HTML from the start, where the browser sees it straight away?
- Does it load right away? Lazy loading would hold it back until it's near the screen.
- Does its tag ask for a high priority, with
fetchpriority="high"?
The photo passed the first two checks and failed the third, so the insight failed. Now look back at the breakdown. The load delay was only 32 ms, so the browser found the photo quickly anyway. A failed insight can point at a small part of the problem. The breakdown tells you how small.
The click, in three parts
Asha's second complaint was the button: she tapped Add to cart, and for a moment nothing happened. That's Interaction to Next Paint, or INP: how long the page took to show a response to a tap, click or key press. The click took 364.1 ms. Up to 200 ms counts as good, so the INP breakdown failed too.
To read its parts, you need one name. The main thread is the browser's head cook: it runs almost all of a page's work, including its JavaScript. The INP breakdown splits the click into three parts:
- Input delay: the click waits for the main thread to finish what it was doing.
- Processing: the page's own code for the click runs. These functions are called event handlers.
- Presentation delay: the browser works out the new layout and draws the next frame, the next picture on the screen.
Its description gives the plan: "Start investigating how to improve INP by looking at the longest subpart". This example finds the longest part:
// The INP breakdown for the click on Add to cart, in milliseconds.const parts = { "input delay": 2.9, "processing": 179.9, "presentation delay": 181.4 };const inp = 364.1; const longestFirst = Object.entries(parts).sort((a, b) => b[1] - a[1]);const [longest, ms] = longestFirst[0];console.log(`INP: ${inp} ms`);console.log(`Start with ${longest}: ${ms} ms, ${Math.round((ms / inp) * 100)}% of INP`);- Line 2 holds the three parts the insight recorded. Line 3 holds the INP itself. The parts add up to 364.2 ms, not 364.1, because each one was rounded.
- Line 5 sorts the parts from longest to shortest. Line 6 takes the first one.
- Line 7 prints
INP: 364.1 ms. Line 8 printsStart with presentation delay: 181.4 ms, 50% of INP.
Processing came a close second, at 179.9 ms. Input delay was only 2.9 ms, so the main thread was free when Asha clicked. The time went on the click itself. Its handler rebuilds a list of prices and draws all 1,800 teas. Then the browser has to lay out and paint those 1,800 teas before the next frame. So the fix lives in the click's own code, and in how much it draws.
Render-blocking requests and document latency
Here's a number the breakdown didn't show. The first paint, when anything at all appeared on the screen, came at 2,819.5 ms. That's the same moment as LCP. For almost three seconds, Asha looked at a white screen. Two insights explain the wait.
Files the first paint waited for
Some files must arrive before the browser paints anything. A stylesheet in the page's <head> is one: the browser won't paint until it knows how things should look. A plain <script> in the head is another. These are render-blocking requests. In the Network track, each one has a red triangle in its upper right corner. Render-blocking and parser-blocking resources explains why they block.
The insight lists them, with the time each took to download:
// The files the first paint waited for, and how long each took to download, in milliseconds.const files = { "app.js": 586, "fonts.css": 582, "styles.css": 582 }; for (const [file, ms] of Object.entries(files)) { console.log(`The first paint waited for ${file}, which took about ${ms} ms.`);}- Line 2 holds the three files the insight found, with the download time it gives each, in milliseconds.
- Lines 4 to 6 print one sentence per file:
The first paint waited for app.js, which took about 586 ms.The first paint waited for fonts.css, which took about 582 ms.The first paint waited for styles.css, which took about 582 ms.
The three downloads ran side by side, so you can't add them up. Instead, the engine estimates the saving as a whole. It simulates the load without the three files, and compares. Its estimate: the first paint would come 1,726 ms sooner. Its estimate for LCP: 0 ms.
Why zero? The engine's source has a rule: "In most cases, render-blocking resources only affect LCP if LCP isn't an image." The store's LCP is the photo, so the rule gives zero. So read an estimate one metric at a time. By the engine's estimate, these files hold back the first paint, not the photo.
The first request of all
Every file waits for the HTML. Document request latency checks that first request. Its description begins: "Your first network request is the most important." It runs three checks:
- Did the request go straight to the page? A redirect is the server saying "it's moved, ask over there", and each one costs a trip.
- Did the server start answering quickly?
- Was the HTML compressed, so there were fewer bytes to send?
The store passed the first and the last. It failed the server check. Here is the rule, taken from DevTools' source:
// Document request latency's rule for the server, from DevTools' source.const tooSlowAbove = 600;const target = 100;const serverResponseMs = 702; const tooSlow = serverResponseMs > tooSlowAbove;const saving = tooSlow ? serverResponseMs - target : 0;console.log(`The server took ${serverResponseMs} ms. Too slow? ${tooSlow}`);console.log(`Estimated saving: ${saving} ms`);- Line 2 is the limit: above 600 ms, the server counts as too slow.
- Line 3 is the target the engine wants you to reach: 100 ms.
- Line 4 is the store's recorded server response: 702 ms. The course's server waited 700 ms of that on purpose.
- Line 6 compares the response with the limit. Line 7 works out the saving: the time above the target, but only when the server was too slow.
- Line 8 prints
The server took 702 ms. Too slow? true. Line 9 printsEstimated saving: 602 ms.
Why two numbers, 600 and 100? The engine's source explains: "Due to the way that DevTools throttling works we cannot see if server response took less than ~570ms." In plain words: with the network slowed down, even a fast server can look like it took about 570 ms. So the engine only fails a server above 600 ms. The source goes on: "We set our failure threshold to 600ms to avoid those false positives but we want devs to shoot for 100ms." A server that answers within 600 ms passes, but 100 ms is the real goal.
The 602 ms counts for the first paint and for LCP alike. Nothing can appear before the HTML does, so a faster server brings everything sooner.
Which insight comes first?
Now you have two estimates. Without the render-blocking files, the first paint could come 1,726 ms sooner. With a faster server, LCP could come 602 ms sooner. Which should the shop fix first? The panel suggests an answer through the order of its list.
Your car goes in for a service. The mechanic hands you a list. Each line is one problem, with how much fixing it would save you on fuel each month. She puts the biggest saving at the top. The checks that found nothing go at the bottom, under "all fine". You can still pick your own order. But you know where the money is.
- In real life: One line on the mechanic's list
- In DevTools: One insight that didn't pass
- In real life: "Fixing this saves you about this much a month"
- In DevTools: Its estimated saving for LCP, INP or CLS
- In real life: The biggest saving, at the top
- In DevTools: The insight the panel lists first
- In real life: "All fine", at the bottom
- In DevTools: The closed Passed insights section
DevTools' trace engine sorts the insights the same way. Its source says it sorts them "based on the impact of each insight's estimated savings". It only counts savings for the three Core Web Vitals: LCP, INP and CLS. CLS, Cumulative Layout Shift, measures content moving on its own. The first paint isn't one of the three, so its 1,726 ms doesn't count. Here is the sort, on three of the store's insights:
// Three insights in the panel's starting order, with their estimated saving for LCP.const savings = { "Render-blocking requests": 0, "Document request latency": 602, "Font display": 0 }; const ranked = Object.entries(savings).sort((a, b) => b[1] - a[1]);for (const [insight, ms] of ranked) { console.log(`${insight}: ${ms} ms`);}- Line 2 lists three insights in the order the engine starts with, each with its estimated saving for LCP.
- Line 4 sorts them, biggest saving first. Insights with equal savings keep their starting order.
- Lines 5 to 7 print
Document request latency: 602 ms, thenRender-blocking requests: 0 msandFont display: 0 ms.
The real panel sorted every insight this way. Document request latency came first, because it was the only insight with a saving for LCP, INP or CLS. The others kept the engine's starting order, which begins with the breakdowns. That's the order in the playground at the top. If you've turned on field data, the engine also gives more weight to the metric your real users struggle with most.
So start at the top of the list. But the order comes from estimates, and an estimate is still a lead.
Layout shift culprits
Asha starts reading about the mint tea. Suddenly the text jumps down, and she loses her place. That's a layout shift: content moving on the screen when nobody touched it. The browser gives each shift a score.
Layout shift culprits groups shifts that come close together into a cluster, and looks at the worst one. Its docs set the bar: a CLS of 0.1 or better is good. The store's worst cluster scored 0.1623, so the insight failed.
Then it does something most insights don't. It names a culprit, a likely cause, for each shift. It knows four kinds: web fonts, iframes (pages inside the page, like ads), animations, and images without a size. Its docs are careful about this: "The insight may offer a best guess for the cause of the observed layout shifts." Here are the worst cluster's three shifts, and its guesses:
// The three shifts in the worst cluster, and what the insight blamed for each.const shifts = [ { at: 3482.5, score: 0.0179, blamed: "the unsized image" }, { at: 3654.3, score: 0.008, blamed: "the unsized image and a web font" }, { at: 4480.5, score: 0.1364, blamed: "the unsized image" },]; const biggest = shifts.reduce((max, shift) => (shift.score > max.score ? shift : max));console.log(`The biggest shift, ${biggest.score}, came at ${biggest.at} ms.`);console.log(`The insight blamed ${biggest.blamed}.`);- Lines 3 to 5 are the three shifts: when each came, its score, and what the insight blamed.
- Line 8 finds the shift with the biggest score.
- Line 9 prints
The biggest shift, 0.1364, came at 4480.5 ms.Line 10 printsThe insight blamed the unsized image.
The unsized image is the photo. But something doesn't add up. The photo was on screen from the first paint, at 2,819.5 ms. The biggest shift came more than a second and a half later. Why would the photo move the page then?
Time to open the bonnet. In the trace, find the shift in the Layout shifts track, and look at what ran on the main thread just before it. There's a Timer Fired event: a setTimeout callback running. It's the store's promotion. Once the HTML has been read, the store's script sets a one-second timer. When the timer fires, the script puts a "Free delivery" box above the photo, with no space kept for it.
// From the trace: when the promotion's timer fired, and when the biggest shift came.const timerFired = 4473.4;const shiftAt = 4480.5; console.log(`The shift came ${(shiftAt - timerFired).toFixed(1)} ms after the timer fired.`);- Lines 2 and 3 are two times from the trace: when the promotion's timer fired, and when the biggest shift came.
- Line 5 subtracts one from the other, and prints
The shift came 7.1 ms after the timer fired.
Here are the three moments, side by side:
The promotion caused the shift. Its box pushed everything below it down by 120 px. So why did the insight blame the photo? Here is its rule, from the engine's source: "An unsized image is considered a root cause if its PaintImage can be correlated to a layout shift." In plain words: if an image without a size is painted again right after a shift, the image gets the blame. The promotion pushed the photo down, so the browser painted the photo again in its new place. The rule saw that paint, and named the photo.
Leaf and Kettle's manager watches the queue on camera. Her rule: note anyone who moves and has no numbered spot on the floor. One morning, a delivery man squeezes in at the front with a big box. Asha, three places back, steps back to make room, and she gets noted. She did move. She didn't cause it.
- In real life: The delivery man squeezing in at the front
- In DevTools: The promotion, put above the content by a timer
- In real life: Asha stepping back to make room
- In DevTools: The photo, pushed down 120 px
- In real life: Having no numbered spot on the floor
- In DevTools: The photo's missing
widthandheight - In real life: The manager's note
- In DevTools: The culprit the insight names
- In real life: Rewinding the footage a few seconds
- In DevTools: Looking at what ran just before the shift
Here's why that matters. The photo still needs a width and height, so the browser can keep its space before it arrives. But adding them wouldn't stop this shift. Only a change to the promotion would. Trust the culprit without checking, and you'd fix the wrong thing first.
Forced reflow and DOM size
Once the page has loaded, reviews.js adds 250 reviews below the teas. To see why an insight flags it, you need one idea first: layout.
Layout is the browser working out the size and position of every box on the page. Firefox calls it reflow, and you'll see both names. Normally, the browser lays out once per frame: first your JavaScript runs, then the browser works out styles, then layout. A script can change the page many times, and the browser still lays out once.
But a script can also ask for a size. scrollHeight, for example, is the full height of an element's content. If the page changed since the last layout, the old answer is out of date. So the browser stops the script and lays out right there. That's a forced reflow. In a loop, it happens again and again.
At Leaf and Kettle's till, Ravi scans a customer's shopping. Usually he adds it all up once, at the end. But today's customer asks "What's the total now?" after every item. Each time, Ravi stops scanning and adds up everything so far. Ten items, ten sums.
- In real life: Scanning one more item
- In JavaScript: Adding one review to the page
- In real life: "What's the total now?"
- In JavaScript: Reading a size, like
scrollHeight - In real life: Ravi stopping to add everything up
- In JavaScript: The browser laying out the page
- In real life: Asking once, at the end
- In JavaScript: Adding every review first, then reading the sizes
Here is the store's script:
// Shows the latest reviews below the teas, each box as tall as its text.function showReviews() { const list = document.createElement("section"); document.querySelector("main").append(list); for (let index = 0; index < 250; index++) { const review = document.createElement("p"); review.textContent = "Review " + (index + 1) + ": fresh, bright mint, just as described."; list.append(review); review.style.height = review.scrollHeight + "px"; }} window.addEventListener("load", showReviews); - Line 13 runs
showReviewsonce the page has loaded. - Lines 3 and 4 make a new section for the reviews, and add it to the page.
- Line 5 starts a loop that runs 250 times. Lines 6 and 7 make one review.
- Line 8 adds the review to the list. That changes the page.
- Line 9 reads the review's
scrollHeight, then sets its height to match. The read comes right after a change, so the browser lays out at once. The new height changes the page again.
That's one forced layout per review. The trace agrees: inside showReviews, there are 250 Layout events. The Forced reflow insight found them, and named the function: showReviews in reviews.js, with 76.4 ms of forced reflow. For those 76.4 ms, the main thread could do nothing else. A tap in that time would have had to wait.
Try both ways in your own browser:
// Measure each review as you add it: a layout for every review.for (const text of texts) { const review = list.appendChild(make(text)); review.style.height = review.scrollHeight + "px";} // Add them all, then measure: one layout for all the reads.const reviews = texts.map((text) => list.appendChild(make(text)));const heights = reviews.map((review) => review.scrollHeight);reviews.forEach((review, index) => (review.style.height = heights[index] + "px"));not yetnot yetEach button adds 400 reviews to the box below, and sizes each one to its text. Reading scrollHeight right after a change makes the browser lay out at once.
How big the page grew
The DOM is the tree of elements the browser builds from your HTML. Scripts change it as the page runs. Optimize DOM size counts it. Its description: "A large DOM can increase the duration of style calculations and layout reflows, impacting page responsiveness."
After the click, the store had 5,664 elements. One parent alone held 1,800 children: the teas that Add to cart draws. This insight was informative, not failed. Still, it explains part of the click. Remember its presentation delay of 181.4 ms? Each of those 1,800 teas had to be styled and laid out before that frame could be drawn.
Images, fonts, and third parties
Three more insights look at what the page downloads, and at who else runs code on it.
A photo four times too wide
The breakdown showed that the photo's download took 62% of LCP. Improve image delivery says why. Its description: "Reducing the download time of images can improve the perceived load time of the page and LCP."
// The photo, as Improve image delivery saw it.const photo = { bytes: 102809, couldSave: 97010, fileWidth: 1600, shownWidth: 380 }; console.log(`${Math.round((photo.couldSave / photo.bytes) * 100)}% of the photo's bytes could go.`);console.log(`It was sent ${Math.round(photo.fileWidth / photo.shownWidth)} times as wide as it was drawn.`);- Line 2 holds what the insight recorded: the photo's size in bytes, the bytes it could save, the file's width, and the width it was drawn at.
- Line 4 prints
94% of the photo's bytes could go. - Line 5 prints
It was sent 4 times as wide as it was drawn.
The file was 1600 by 1067 pixels, shown at 380 by 253. That's like posting someone a poster when they asked for a postcard. A file sized for its box would have far fewer bytes to send over Slow 4G. Responsive images shows how to send one.
Text that waits for its font
The store's headings use the Geist web font. In the CSS that loads a font, a font-display setting tells the browser what to show while the font downloads. The store didn't set one, so it got auto, which leaves the choice to the browser. The browser may keep the text invisible until the font arrives. It's like a café that leaves its menu board blank until the fancy chalk is delivered.
Font display failed. It counted the font's whole download as wasted time: 735 ms. Its description suggests two other values, "to ensure text is consistently visible". Exercise 4 asks you to find them.
Code from other companies
A third party is anyone whose code runs on your page but isn't you: an ad network, an analytics tool, a chat widget. 3rd parties lists each one with how much it sent and how much main-thread time it used. It's informative: it reports, without judging.
In the recording, the store came from 127.0.0.1 and the chat widget from localhost. Those are two different addresses, so the engine counts the widget as a third party.
// Main-thread time for each origin, from the 3rd parties insight, in milliseconds.const mainThreadMs = { store: 350.1, chatWidget: 114.4 };const counted = mainThreadMs.store + mainThreadMs.chatWidget; const share = Math.round((mainThreadMs.chatWidget / counted) * 100);console.log(`The chat widget used ${share}% of the main-thread time the insight counted.`);- Line 2 holds the main-thread time the insight gave each one, in milliseconds.
- Line 3 adds them up. Line 5 works out the widget's share.
- Line 6 prints
The chat widget used 25% of the main-thread time the insight counted.
A quarter of that time went on code the shop didn't write. That's worth knowing before you add another widget.
Asking AI assistance, and checking its answer
There's one more place to get a lead. Expand an insight in the panel, and you'll see a Debug with AI button. It opens the AI assistance panel, powered by Gemini, Google's AI model. The insight is already chosen as the topic. You can ask why it failed, or what to change.
AI assistance is off until you turn it on, in Settings, under AI assistance. DevTools' docs are plain about what it is: "This feature is experimental and subject to future changes. It may generate inaccurate or offensive information that doesn't represent Google's views."
So treat its answer like directions from a stranger who sounds very sure. They may be right. You still check the map. The checks are the four steps from the picture at the top:
- Find the numbers it quotes in the insights. Do they match?
- Find the events in the trace, and look at what ran just before them.
- Change the one thing it blames.
- Record again. If the number doesn't move, the answer was wrong, however sure it sounded.
The culprit you checked earlier came from a plain rule, with no AI at all, and it was wrong. So every lead gets the same four checks, whether it comes from a rule or from a chat.
Here is everything this lesson read, and how far to trust each. Read each row across:
| What you read | What it tells you | How far to trust it |
|---|---|---|
| A metric, like LCP | How slow this recording was, as one number. | It was measured, so it's what happened on this run. |
| An insight | A likely cause, found by a check. | It's a lead. Look at its evidence in the trace. |
| An estimated saving | How much a fix might help, from a rule or a simulation. | It's a forecast. A new recording gives the real number. |
| An AI answer | An explanation in words. | It's a lead too. The docs call the feature experimental. |
| The trace | What ran, and when. | It's the evidence. |
Which metric's insight?
Most insights in this lesson point at one of the three metrics. Ask what the problem does to Asha. Does it delay the main content? That's LCP. Does it move content on its own? That's CLS. Does it slow the response to her tap? That's INP.
- The server takes 700 ms to send the HTML
- The hero photo is 1600 pixels wide but shown at 380
- The LCP image has no fetchpriority="high"
- A banner appears above the content a second after load
- An image without width and height arrives late
- A click handler redraws 1,800 cards
- The main thread is busy when the user taps
Sort each problem by the metric whose insights would flag it.
Common mix-ups
- "A failed insight is the cause." No. It's a likely cause. LCP request discovery failed, but the photo's load delay was only 32 ms.
- "The culprit it names is always right." No. It's a best guess. Layout shift culprits blamed the photo, and the promotion caused the shift.
- "An estimated saving is a promise." No. It comes from a rule or a simulation. Only a new recording shows what you really saved.
- "Only the top insight matters." No. The order only counts savings for LCP, INP and CLS. Render-blocking requests could bring the first paint 1,726 ms sooner, and still sat lower down.
- "A big DOM only matters while the page loads." No. The 1,800 teas came from a click, and they gave that click more to draw.
- "Forced reflow is a CSS problem." No. It's JavaScript reading a size right after changing the page. Here,
scrollHeightafter each new review. - "AI assistance checks its own answers." No. The docs call it experimental, and say it may be inaccurate.
Practice exercises
Answer first, then use the hints.
The server now answers in 450 ms. What does the second line print?
// Document request latency's rule for the server, from DevTools' source.
const tooSlowAbove = 600;
const target = 100;
const serverResponseMs = 450;
const tooSlow = serverResponseMs > tooSlowAbove;
const saving = tooSlow ? serverResponseMs - target : 0;
console.log(`The server took ${serverResponseMs} ms. Too slow? ${tooSlow}`);
console.log(`Estimated saving: ${saving} ms`);Estimated saving: 0 ms. 450 ms is under the 600 ms limit, so the insight doesn't call the server too slow, and estimates no saving. The engine would still like 100 ms.
The last line now rounds LCP to whole milliseconds. What does it print?
// The LCP breakdown for the photo, in milliseconds.
const parts = { "Time to first byte": 703.2, "Load delay": 32, "Load duration": 1734.4, "Render delay": 349.9 };
const lcp = Object.values(parts).reduce((sum, ms) => sum + ms, 0);
for (const [part, ms] of Object.entries(parts)) {
console.log(`${part}: ${ms} ms, ${Math.round((ms / lcp) * 100)}% of LCP`);
}
console.log(`LCP: ${Math.round(lcp)} ms`);LCP: 2820 ms. Math.round rounds a half up, so 2819.5 becomes 2820.
In the insights playground, how many bytes did the chat widget, from localhost, send?
1,028 bytes. The store sent 287,944. So the widget was tiny to download, yet it used a quarter of the main-thread time. A small file can still cost a lot to run.
The Font display insight suggests two values for font-display. Which two?
swap or optional. The docs say either one passes the insight. Both are there "to ensure text is consistently visible".
A faster server brings Document request latency's saving down to 0. What does the first line print now?
// Three insights in the panel's starting order, with their estimated saving for LCP.
const savings = { "Render-blocking requests": 0, "Document request latency": 0, "Font display": 0 };
const ranked = Object.entries(savings).sort((a, b) => b[1] - a[1]);
for (const [insight, ms] of ranked) {
console.log(`${insight}: ${ms} ms`);
}Render-blocking requests: 0 ms. With every saving at 0, the sort keeps the starting order. In the real panel, Document request latency would also pass, since its other two checks passed too. It would move down into Passed insights.
How many times does this loop force the browser to lay out the page? Then rewrite it so it forces only one layout.
// Three reviews, each sized to its text as it's added.
const list = document.querySelector("#reviews");
for (const text of ["Fresh", "Bright", "Calm"]) {
const review = document.createElement("p");
review.textContent = text;
list.append(review);
review.style.height = review.scrollHeight + "px";
}// Add all three reviews first.
const list = document.querySelector("#reviews");
const reviews = ["Fresh", "Bright", "Calm"].map((text) => {
const review = document.createElement("p");
review.textContent = text;
list.append(review);
return review;
});
// Then read every height, and only then change them.
const heights = reviews.map((review) => review.scrollHeight);
reviews.forEach((review, index) => (review.style.height = heights[index] + "px"));3. Each pass of the loop adds a review, then reads a size, so the browser lays out three times. The solution adds all three reviews first. Then it reads all three heights, and only then sets them. The first read lays out once. The other two find the layout still up to date.
Check your understanding
For each question, ask three things. What did the insight measure? Where is its evidence? Does the evidence agree?
Question 1 of 7What does the third line of the LCP breakdown example print?
Read the code, then predict// The LCP breakdown for the photo, in milliseconds. const parts = { "Time to first byte": 703.2, "Load delay": 32, "Load duration": 1734.4, "Render delay": 349.9 }; const lcp = Object.values(parts).reduce((sum, ms) => sum + ms, 0); for (const [part, ms] of Object.entries(parts)) { console.log(`${part}: ${ms} ms, ${Math.round((ms / lcp) * 100)}% of LCP`); } console.log(`LCP: ${lcp.toFixed(1)} ms`);Choose an answer to see the explanation.
Question 2 of 7What does an insight's "fail" state tell you?
Choose an answer to see the explanation.
Question 3 of 7What does the second line of the culprit example print?
Read the code, then predict// The three shifts in the worst cluster, and what the insight blamed for each. const shifts = [ { at: 3482.5, score: 0.0179, blamed: "the unsized image" }, { at: 3654.3, score: 0.008, blamed: "the unsized image and a web font" }, { at: 4480.5, score: 0.1364, blamed: "the unsized image" }, ]; const biggest = shifts.reduce((max, shift) => (shift.score > max.score ? shift : max)); console.log(`The biggest shift, ${biggest.score}, came at ${biggest.at} ms.`); console.log(`The insight blamed ${biggest.blamed}.`);Choose an answer to see the explanation.
Question 4 of 7In the recording, why did Layout shift culprits blame the photo for the promotion's shift?
Choose an answer to see the explanation.
Question 5 of 7Why did Document request latency come first in the store's list of insights?
Choose an answer to see the explanation.
Question 6 of 7What does the Forced reflow insight look for?
Choose an answer to see the explanation.
Question 7 of 7AI assistance says your LCP is slow because of a third-party script. What do you do first?
Choose an answer to see the explanation.
Key takeaways
- An insight is a check DevTools runs over a recording. It fails, informs or passes, and names its evidence.
- The LCP and INP breakdowns split one number into parts. Start with the longest part.
- The panel lists insights by their estimated savings for LCP, INP and CLS. Passed ones fold away at the bottom.
- An estimated saving comes from a rule or a simulation. A new recording gives the real number.
- A culprit is a best guess. Check what ran just before the shift.
- A forced reflow is a script reading a size right after changing the page.
- AI assistance gives leads too, and its docs call it experimental.
Remember the one-liner.
An insight is a warning light: it tells you where to look, the trace shows what's wrong, and a new recording proves the fix.
Sources
The facts on this page come from these docs, articles and source files:
- Performance Insights, Chrome for Developers.
- Performance features reference: Get actionable insights, Chrome DevTools.
- The insights' source code, devtools-frontend.
- How the trace engine sorts insights (sortInsightSet), devtools-frontend.
- @paulirish/trace_engine, npm, the trace engine used by the DevTools Performance panel.
- LCP breakdown, Connor Clark, Chrome for Developers.
- INP breakdown, Connor Clark, Chrome for Developers.
- Layout shift culprits, Connor Clark, Chrome for Developers.
- Forced reflow, Connor Clark, Chrome for Developers.
- Avoid large, complex layouts and layout thrashing, web.dev.
- CSS Fonts Module Level 4: the font-display descriptor, W3C.
- Chat with AI assistance, Matthias Rohmer and Sofia Emelianova, Chrome DevTools.
- AI assistance, Chrome DevTools.
Coming next: The Network panel for performance, also in Measuring in the lab. You'll read each request's timing, priority and initiator, block requests to test their cost, and share HAR files.