completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

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.

By the end, you can
  • 01
    Read the insightsSay what each insight checks, from the LCP breakdown to 3rd parties, and what fail, informative and pass mean.
  • 02
    Find the evidenceGo from an insight to the requests, tasks and layout shifts in the trace that it points at.
  • 03
    Check 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:

The Performance panel with the store's recording. On the right, the recording: 59,581 events. On the left, the Insights tab lists 12 insights, in this order: Document request latency, fail; INP breakdown, fail; LCP breakdown, fail; LCP request discovery, fail; Layout shift culprits, fail; Render-blocking requests, fail; Network dependency tree, fail; Improve image delivery, fail; Font display, fail; Optimize DOM size, informative; 3rd parties, informative; Forced reflow, fail. Below them, a closed Passed insights section holds 7 more.Performance panelInsightsfailDocument request latencyslow serverfailINP breakdownfailLCP breakdownfailLCP request discoveryfailLayout shift culpritsfailRender-blocking requestsfailNetwork dependency treefailImprove image deliveryphoto too bigfailFont displayinformativeOptimize DOM sizeinformative3rd partiesfailForced reflow▸ Passed insights (7)the recording59,581 eventswhere do you start?the panelreads it
The store's recording in the Performance panel, drawn from the saved trace. The Insights tab lists the 12 insights that didn't pass, in the panel's order, each with its state. The 7 that passed are folded away. Document request latency is the slow server. Improve image delivery is the photo sent too big.

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

Real-life analogyInsights are a car's warning lights

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:

Playground: the insights sidebar
Document request latencyfail
Server response702 ms
Redirectsnone
Compressionfine
Try it yourself
Document request latency

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

DevTools' own trace engine worked these out from a real recording of the store. It used a phone-sized screen, Slow 4G, and a CPU 4 times slower. Of five loads, it's the one with the middle LCP.
The main idea

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.

Checking an insight in four steps: read it, look at the evidence it points to in the trace, make one change, and record again to see whether the number moved.insightits evidenceone changerecord again
Four steps for every lead. The insight says where to look. The trace shows what happened. A new recording shows whether your change helped.

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.

Real-life analogyWhere the wait for a cup of tea went

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.
The recorded LCP of 2819.5 ms in four subparts: time to first byte 703.2 ms, load delay 32 ms, load duration 1734.4 ms, and render delay 349.9 ms.TTFB703.2 msload delay 32 msload duration1734.4 msrender delay 349.9 ms
Each part is as wide as the time it took. The photo's download is the widest. The wait for the server comes first.

This example prints the store's four parts, and each part's share of the whole:

Where the photo's 2,819.5 ms wentPop out in the code editor (opens in a new tab)JavaScript
// 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 LCP and Load delay: 32 ms, 1% of LCP. Then come Load duration: 1734.4 ms, 62% of LCP and Render 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 click's longest partPop out in the code editor (opens in a new tab)JavaScript
// 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 prints Start 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:

What the first paint waited forPop out in the code editor (opens in a new tab)JavaScript
// 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:

How the insight judges the serverPop out in the code editor (opens in a new tab)JavaScript
// 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 prints Estimated 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.

Real-life analogyA mechanic's list, biggest saving first

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:

Biggest saving firstPop out in the code editor (opens in a new tab)JavaScript
// 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, then Render-blocking requests: 0 ms and Font 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 worst cluster, and what got the blamePop out in the code editor (opens in a new tab)JavaScript
// 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 prints The 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.

What ran just before the shiftPop out in the code editor (opens in a new tab)JavaScript
// 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 photo was painted at 2,819.5 ms. The promotion's timer fired at 4,473.4 ms, and the biggest layout shift came at 4,480.5 ms. The insight blamed the photo.Photo painted2,819.5 mson screen from the first paintTimer Fired4,473.4 msthe promotion arrivesLayout shift4,480.5 msblamed on the photo7.1 ms
The photo had been on screen for more than a second and a half. The shift came 7.1 ms after the promotion's timer fired, yet the insight blamed the photo.

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.

Real-life analogyBlaming whoever moved

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 width and height
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.

Real-life analogyAsking for the total after every item

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:

reviews.jsJavaScript
// 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 showReviews once 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:

Playground: a forced reflow
The two buttons, shortenedJavaScript
// 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"));
400 reviewslive
Measure each reviewnot yet
Add all, then measurenot yet
Try it yourself
Your browser

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

This runs in your browser, on the box below. Times depend on your device, so run each button a few times.

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, too big for its boxPop out in the code editor (opens in a new tab)JavaScript
// 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.

The widget's share of the main threadPop out in the code editor (opens in a new tab)JavaScript
// 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:

  1. Find the numbers it quotes in the insights. Do they match?
  2. Find the events in the trace, and look at what ran just before them.
  3. Change the one thing it blames.
  4. 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:

Five things to read, and how far to trust each
What you readWhat it tells youHow far to trust it
A metric, like LCPHow slow this recording was, as one number.It was measured, so it's what happened on this run.
An insightA likely cause, found by a check.It's a lead. Look at its evidence in the trace.
An estimated savingHow much a fix might help, from a rule or a simulation.It's a forecast. A new recording gives the real number.
An AI answerAn explanation in words.It's a lead too. The docs call the feature experimental.
The traceWhat 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.

Which metric's insight?
  • 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
Try it yourself
0 of 7 correct

Sort each problem by the metric whose insights would flag it.

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

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, scrollHeight after 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.

Exercise 1 · Warm-upA faster server

The server now answers in 450 ms. What does the second line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// 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`);

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

    Exercise 2 · Warm-upWhole milliseconds

    The last line now rounds LCP to whole milliseconds. What does it print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // 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`);

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

      Exercise 3 · PracticeThe widget's bytes

      In the insights playground, how many bytes did the chat widget, from localhost, send?

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

        Exercise 4 · PracticeBetter values

        The Font display insight suggests two values for font-display. Which two?

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

          Exercise 5 · ChallengeA new top of the list

          A faster server brings Document request latency's saving down to 0. What does the first line print now?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          // 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`);
          }

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

            Exercise 6 · ChallengeMeasure once

            How many times does this loop force the browser to lay out the page? Then rewrite it so it forces only one layout.

            Starter codeJavaScript
            // 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";
            }

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

              Check your understanding

              For each question, ask three things. What did the insight measure? Where is its evidence? Does the evidence agree?

              Performance insights quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the third line of the LCP breakdown example print?

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

              2. Question 2 of 7What does an insight's "fail" state tell you?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the second line of the culprit example print?

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

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

              5. Question 5 of 7Why did Document request latency come first in the store's list of insights?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does the Forced reflow insight look for?

                Choose an answer to see the explanation.

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

              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.

              CompleteFrontend Clear concepts. Working examples.