# Web Performance and Core Web Vitals

> What actually makes a page feel fast: LCP, CLS, and INP, the Network tab and bundle size, and the fixes that move the numbers.


---

# Web Performance and Core Web Vitals

Your page "works." It loads, eventually. But someone - a user, a manager, a Lighthouse report glaring red - says it feels slow, and you're not sure what that means or what to touch first. You open dev tools, see a wall of numbers, and close it again.

Here's the relief: web performance isn't a vague vibe, and it isn't a thousand micro-tricks. It's a small set of things users actually feel, three numbers that measure those feelings, and a short list of levers that move them. This guide gives you the mental model, the everyday tools, and the fixes that actually pay - so the next time a page feels slow, you know exactly where to look.

## How to read this

- **Want it to finally make sense?** Read in order. Each phase builds on the last, and it's short.
- **Already have a bad score and need the fixes?** Jump to [Phase 3: The Levers That Move the Numbers](03-the-levers-that-move-the-numbers.md).
- **Confused about why your local test and the real score disagree?** That's [Phase 2: Measuring What Users Feel](02-measuring-what-users-feel.md).

## The phases

1. **[Perceived Performance and the Three Vitals](01-perceived-performance-and-the-three-vitals.md)** - performance is what the user *feels*, not what a server log says. The three Core Web Vitals - LCP, CLS, and INP - turn those feelings into numbers you can chase.
2. **[Measuring What Users Feel](02-measuring-what-users-feel.md)** - lab versus field data, why Lighthouse and real users disagree, and how to read the Network tab to see where the time and the bytes actually go.
3. **[The Levers That Move the Numbers](03-the-levers-that-move-the-numbers.md)** - the fixes that pay: bundle size and code splitting, images, caching and a CDN, render-blocking resources, and sizing media so the layout stops jumping.

> This guide assumes you already know what "fast" means in the abstract - latency, throughput, measure-before-you-optimize. If that's shaky, start with [What "Performance" Even Means](/guides/what-performance-means). For the disciplined loop that turns a measurement into durable speed, see [Optimizing Real Systems](/guides/optimizing-real-systems).


---

# Perceived Performance and the Three Vitals

Picture two pages. The first finishes loading in 800 milliseconds, but for the first half-second it's a blank white screen, then everything pops in at once and a banner shoves the button you were about to tap. The second takes a full second to "finish," but the headline and main image appear almost instantly, nothing jumps around, and every tap responds the moment you make it.

Ask a real user which one is faster and they'll pick the second one - even though, by the stopwatch, it's slower. That gap between *what the clock says* and *what the user feels* is the single most important idea in web performance. The server's "request completed in 200ms" log is real, but it's not the experience. The experience is built from a handful of moments the user lives through: when did I see something? Did it stay put? When I tapped, did it respond?

So here's the mental model for this whole guide: **you are optimizing a feeling, and the feeling has been broken into three measurable moments.** Google calls them the Core Web Vitals. Each one captures a different way a page can feel bad, and each one is a number you can measure and move.

## The user's timeline, in three questions

Every page load is a small story the user experiences in order. Each Core Web Vital answers one question in that story.

```text
   user clicks a link
        │
        ▼
   [ blank ] ──▶ "When does the MAIN thing appear?"      → LCP   (loading)
        │
        ▼
   [ content paints ] ──▶ "Does it STAY PUT, or jump?"    → CLS   (visual stability)
        │
        ▼
   [ user taps a button ] ──▶ "Does it RESPOND quickly?"  → INP   (interactivity)
```

*What just happened:* The three vitals aren't three random metrics - they're three points along the one timeline every user moves through. Loading (LCP), stability (CLS), and responsiveness (INP) are the three distinct ways that timeline can feel slow or broken, which is exactly why these three were chosen.

## LCP - Largest Contentful Paint (the loading feeling)

LCP measures **how long until the biggest piece of content visible in the viewport has rendered.** Not the first pixel, not the last byte - the largest *meaningful* element: usually the hero image, a big heading, or the main block of text. That's the moment the user feels "okay, the page is here."

Why the *largest* element? Because that's the proxy for "the main content has arrived." A spinner appearing fast doesn't help; the user is waiting for the actual thing. LCP captures the arrival of the actual thing.

```text
   Good       ≤ 2.5 s
   Needs work 2.5 s – 4.0 s
   Poor       > 4.0 s
```

*What just happened:* These are the thresholds Google publishes for LCP, measured at the 75th percentile of real page loads (more on that percentile in [Phase 2](02-measuring-what-users-feel.md)). "Good" means at least 75% of your visitors saw the main content within 2.5 seconds. The common LCP killers - a huge unoptimized hero image, a slow server response, render-blocking scripts - are exactly the levers we fix in [Phase 3](03-the-levers-that-move-the-numbers.md).

## CLS - Cumulative Layout Shift (the stability feeling)

CLS measures **how much the visible content moves around unexpectedly while the page is loading.** You've felt this: you go to tap a link, an image finishes loading above it, the whole page jumps down, and you tap an ad instead. That lurch is a layout shift, and CLS adds them all up.

It's not measured in seconds - it's a unitless score combining *how much* of the screen moved by *how far*. A perfectly stable page scores 0. The more things jump, and the bigger the jumps, the higher (worse) the score.

```text
   Good       ≤ 0.1
   Needs work 0.1 – 0.25
   Poor       > 0.25
```

*What just happened:* These are the CLS thresholds. The cause is almost always the same: an element with no reserved space - an image without width and height, an ad slot, a late-loading font or banner - drops in and pushes everything else down. The fix is to tell the browser how big things will be *before* they load, so the space is held open. We cover sized media in [Phase 3](03-the-levers-that-move-the-numbers.md).

> 💡 **Why "cumulative."** A single shift might be small, but a page that nudges itself five times during load feels broken even if no single jump is large. CLS sums the shifts across the whole load to capture that death-by-a-thousand-jumps feeling.

## INP - Interaction to Next Paint (the responsiveness feeling)

INP measures **how quickly the page responds visually after the user interacts** - a click, a tap, a key press. Specifically, the time from the interaction until the browser paints the next frame showing a response. It looks across all the interactions in a visit and reports a number close to the worst one, because the worst lag is the one users remember.

This is the vital that catches "I clicked and nothing happened." Usually the culprit is JavaScript: a long task hogging the main thread, so the browser can't get around to handling your click and updating the screen.

```text
   Good       ≤ 200 ms
   Needs work 200 ms – 500 ms
   Poor       > 500 ms
```

*What just happened:* These are the INP thresholds, in milliseconds. INP replaced an older metric (First Input Delay) in 2024 because FID only measured the *first* interaction's delay; INP looks at responsiveness across the whole visit, which is a far truer picture of how the page felt to use. Heavy JavaScript bundles are the usual cause - another reason bundle size in [Phase 3](03-the-levers-that-move-the-numbers.md) matters so much.

## For builders: the three are independent - and you need all three

A page can ace one vital and fail another, and they fail for different reasons. A blog with a giant hero image has bad LCP but probably fine INP. A heavy single-page app might paint fast (good LCP) but choke on every click (bad INP). An ad-heavy news site can load and respond fine yet shove content around constantly (bad CLS).

That's the practical payoff of splitting the feeling into three: when someone says "the page is slow," you don't guess. You check which vital is red, and that tells you *which kind* of slow it is - loading, stability, or responsiveness - and therefore which family of fixes to reach for. Before you can fix anything, though, you have to measure it straight. That's where local tools lie to you, and where field data tells the truth - Phase 2.

```quiz
[
  {
    "q": "A page finishes loading by the stopwatch in 700ms but shows a blank screen for most of that time, then pops everything in at once. Why might users still call it slow?",
    "choices": [
      "The stopwatch is wrong; 700ms is impossible",
      "Perceived performance is about what the user feels seeing content, not when the server says it's done",
      "Throughput is too low",
      "The page has too many DNS lookups"
    ],
    "answer": 1,
    "explain": "Performance is the experience, not the server log. A blank screen that fills in late feels slow even with a fast total time - which is exactly what LCP captures."
  },
  {
    "q": "Which Core Web Vital captures 'I tapped a button and nothing happened for a moment'?",
    "choices": [
      "LCP (Largest Contentful Paint)",
      "CLS (Cumulative Layout Shift)",
      "INP (Interaction to Next Paint)",
      "TTFB (Time to First Byte)"
    ],
    "answer": 2,
    "explain": "INP measures the time from an interaction to the next visual response. Laggy clicks, usually caused by heavy JavaScript blocking the main thread, show up as poor INP."
  },
  {
    "q": "A 'Good' CLS score is at or below 0.1. What does a CLS score actually measure?",
    "choices": [
      "Seconds until the largest element renders",
      "How much visible content moves around unexpectedly during load",
      "Milliseconds of delay before the first click is handled",
      "The total number of network requests"
    ],
    "answer": 1,
    "explain": "CLS is a unitless score summing how much of the viewport shifted and how far, capturing the lurch when unsized images, ads, or fonts drop in and push content around."
  }
]
```


---

# Measuring What Users Feel

You run Lighthouse on your laptop. Green across the board, 98. You ship it, feeling good. A week later someone pulls up the real-user dashboard and your LCP is in the red. Same page, same code - wildly different numbers. What happened?

Nothing went wrong. You measured two different things and assumed they were the same. This is the trap that wastes the most time in web performance: trusting one measurement, in one environment, as if it were the truth. Your laptop on office wifi is not your user on a three-year-old phone on a train. Until you know which kind of measurement you're looking at, you can't tell whether a number is good news, bad news, or noise.

So the mental model for this phase: **there are two kinds of performance data, and they answer different questions.** Lab data answers "is this page *capable* of being fast, in a controlled setting?" Field data answers "is this page *actually* fast for the humans using it?" You need both, and you need to stop confusing them.

## Lab data versus field data

```text
   LAB DATA (synthetic)              FIELD DATA (real users / RUM)
   ─────────────────────            ──────────────────────────────
   one device, one network          thousands of real devices/networks
   you control everything           you control nothing
   repeatable, debuggable           messy, representative
   "CAN it be fast?"                "IS it fast, for real people?"
   e.g. Lighthouse, WebPageTest     e.g. Chrome UX Report (CrUX), RUM
```

*What just happened:* The two columns aren't rivals - they're a division of labor. Lab data is your workshop: clean, controlled, where you reproduce a problem and test a fix. Field data is your reality: the actual distribution of devices, networks, and patience your users bring. A fix proven in the lab still has to show up in the field to count. Google's official Core Web Vitals - the ones that affect search ranking - are measured from **field data**, not your laptop.

> 💡 **Lighthouse is lab data.** That 98 in your dev tools is a simulation on *your* machine under *one* throttling profile. It's genuinely useful for finding problems and checking fixes - but it is not what your users experience, and it is not what Google ranks on. When lab and field disagree, the field is the one that's right about reality.

## The 75th percentile, not the average

Field data never gives you one number - it gives you a distribution, because your users are all different. Core Web Vitals are reported at the **75th percentile**: the value that 75% of page loads came in *at or below*.

Why not the average? Because averages hide the people having a bad time. If most visits are fast but a meaningful slice are awful, the average can look fine while a quarter of your users suffer. The 75th percentile says "three out of four of your users had at least this good an experience" - and it deliberately keeps the slow tail in view.

```text
   100 page loads, sorted by LCP (fastest → slowest)
   ▕▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▕▔▔▔▔▔▔▔▔▔▕
                                                              ▲
                                               75th percentile lives here
                          (ignore the average - watch this and the slow tail to its right)
```

*What just happened:* The metric is taken three-quarters of the way along the sorted distribution, not at the middle and not at the average. This is the same percentile thinking that runs through all of performance work - watch the tail, not the mean - covered more deeply in [Optimizing Real Systems](/guides/optimizing-real-systems). It's why a green Lighthouse run can coexist with a red field score: your laptop is one fast sample; the 75th percentile includes the slow phones you never test on.

## Reading the Network tab

When LCP is bad and you need to know *why*, the Network tab in your browser's dev tools is the first place to look. It shows every resource the page requested - HTML, CSS, JavaScript, images, fonts - as a waterfall: when each one started, how long it took, and how big it was.

```text
   Name              Size     Time     Waterfall
   ───────────────────────────────────────────────────────────
   document.html     14 kB    220 ms   ▕█▏                          ← TTFB: server think time
   app.bundle.js    480 kB    900 ms       ▕████████▏               ← huge JS, blocks rendering
   styles.css        38 kB    140 ms       ▕█▏
   hero.jpg        1,800 kB  1,400 ms          ▕███████████▏        ← unoptimized image = LCP killer
   font.woff2        96 kB    180 ms              ▕█▏
   ───────────────────────────────────────────────────────────
   Total: 2.4 MB transferred, finished at ~2.6 s
```

*What just happened:* Two problems jump out of this waterfall. The 480 kB JavaScript bundle and the 1.8 MB hero image dominate both the bytes and the timeline - and that hero image is almost certainly the LCP element, so its 1.4-second download *is* your bad LCP. The waterfall turns a vague "the page is slow" into "these two specific resources are the cost," which is the whole point of looking.

A few things to read off every waterfall:

- **TTFB (Time to First Byte)** - how long before the server even started sending the HTML. A long bar before *anything* else means the problem is the server or network, not the front-end. Nothing you do to images will help a slow TTFB.
- **Size column** - the actual transferred bytes (after compression). This is your bundle-size and image-weight reality check. Sort by it.
- **The long bars** - the resources that take the most time. These are your suspects, ranked for free.
- **Blocking resources** - CSS and synchronous JavaScript in the `<head>` that the browser must fetch and process *before* it can paint. We disarm these in [Phase 3](03-the-levers-that-move-the-numbers.md).

## Throttle, or you're lying to yourself

Your dev machine has a fast CPU and a fast connection. Your users, on average, do not. Every dev tools Network tab has a throttling dropdown - set it to something like "Slow 4G" and, where available, throttle the CPU too. The page you thought was instant will suddenly behave like it does for a real person on a mid-range phone.

> ⚠️ **The fast-laptop illusion.** The single most common reason a team is blindsided by bad field data is that everyone tests on fast hardware and fast wifi. Throttling isn't pessimism - it's the closest a lab tool gets to telling the truth. Make it a habit before you call anything "fast."

## For builders: measure, fix, then confirm in the field

Put the two kinds of data to work in a loop. Use **field data** (CrUX, or real-user monitoring if you have it) to decide *whether* there's a problem and which vital is red. Use **lab tools** (Lighthouse, the Network tab, throttled) to *diagnose* it - find the heavy resource, the blocking script, the unsized image - and to verify your fix made the number move locally. Then wait for the field data to confirm the win for real users, because field is the scoreboard that counts.

That's the no-nonsense measurement discipline. Now that you can see *where* the time and bytes go, Phase 3 is the short list of levers that move them.

```quiz
[
  {
    "q": "Your Lighthouse score is a green 98, but Google's Core Web Vitals report shows your LCP in the red. What's the most likely explanation?",
    "choices": [
      "Lighthouse is broken and should be ignored",
      "Lighthouse is lab data from your fast machine; Google ranks on field data from real users' slower devices",
      "Your server is down",
      "The two tools measure completely unrelated things and can never agree"
    ],
    "answer": 1,
    "explain": "Lighthouse is a controlled lab simulation on your hardware. Core Web Vitals are measured from real users in the field - slower phones and networks - so a green lab score can coexist with a red field score."
  },
  {
    "q": "Why are Core Web Vitals reported at the 75th percentile instead of the average?",
    "choices": [
      "The 75th percentile is easier to compute",
      "Averages can look fine while hiding a large slice of users having a bad experience; the percentile keeps the slow tail in view",
      "It makes the numbers look better",
      "Browsers can only measure percentiles"
    ],
    "answer": 1,
    "explain": "An average can mask a suffering minority. The 75th percentile guarantees at least three of four users had that experience or better, while deliberately keeping the slow tail visible."
  },
  {
    "q": "In the Network tab, you see a long bar before any other resource loads, even the HTML. What does that point to?",
    "choices": [
      "An oversized hero image",
      "A bloated JavaScript bundle",
      "A slow TTFB - the server or network is slow to send the first byte, before the front-end is even involved",
      "Too much CSS"
    ],
    "answer": 2,
    "explain": "Time to First Byte is the delay before the server starts responding. A long gap before anything arrives means the bottleneck is server/network - optimizing images or JS won't help it."
  }
]
```


---

# The Levers That Move the Numbers

You've measured. You know which vital is red and you've seen the heavy resources in the waterfall. Now comes the part everyone wants to skip to - the fixes. The good news is there's no thousand-item checklist. A small number of levers account for the overwhelming majority of real wins, and each one maps to a vital you already understand.

The trap here is the same one from [Optimizing Real Systems](/guides/optimizing-real-systems): don't reach for a lever before you've confirmed it's the bottleneck. Shrinking a bundle that was already small, or hand-tuning an image on a page whose problem is the server, is motion without progress. Pull the lever the measurement pointed you at.

Here's the map. Each lever, and the vital it moves:

```text
   LEVER                          MOVES        WHY
   ──────────────────────────────────────────────────────────────
   1. Bundle size / code split    INP, LCP     less JS to parse & run
   2. Image optimization          LCP          the hero is usually the bottleneck
   3. Caching + a CDN             LCP, TTFB    bytes start closer & arrive cached
   4. Kill render-blocking        LCP          let the browser paint sooner
   5. Size your media             CLS          reserve space so nothing jumps
```

*What just happened:* The five levers cover all three vitals, with no overlap you have to memorize. JavaScript weight hurts responsiveness and loading; images and delivery hurt loading; unsized media hurts stability. Match the red vital to its levers and you've narrowed a vague task to one or two concrete moves.

## 1. Bundle size and code splitting

JavaScript is the most expensive kind of byte on the web. An image of the same size only gets decoded; a JavaScript bundle has to be downloaded, *parsed*, *compiled*, and *executed* - all on the main thread, the same thread that handles your user's clicks. That's why a fat bundle wrecks INP and drags LCP: while the browser chews through your JS, it can't paint and it can't respond.

The first move is to **ship less**. Audit what's in the bundle (most bundlers have an analyzer), drop dependencies you don't need, and prefer a small library over a kitchen-sink one. A surprising amount of bundle weight is a date library or a UI kit you use one function from.

The second move is **code splitting**: instead of one giant bundle the user downloads up front, break it into pieces and load each piece only when it's needed.

```text
   BEFORE: one bundle, everything up front
   app.bundle.js  ▕████████████████████████▏  480 kB  ← user waits for ALL of it

   AFTER: split by route, load on demand
   home.js        ▕████▏                         80 kB  ← only this loads on the home page
   checkout.js    ▕██████▏          (loaded later, only when they visit checkout)
   admin.js       ▕████████▏        (most users never load this at all)
```

*What just happened:* The home page now ships 80 kB instead of 480 kB, because the checkout and admin code load lazily, only when a user actually goes there. Many users never touch those routes, so that code is work you never do for them - the "do less work" principle, applied to bytes. Less JavaScript up front means faster parsing: a faster LCP and a main thread free for clicks.

## 2. Images - usually your LCP element

On most content pages the largest contentful element is an image: the hero, the product shot, the article header. That means image weight *is* your LCP. The fixes are well-trodden and they stack:

- **Compress and resize.** Don't ship a 3000-pixel-wide image into a 800-pixel-wide slot. Resize to the size actually displayed and compress it. This alone often cuts an image to a fraction of its weight.
- **Use a modern format.** WebP and AVIF deliver the same visual quality as JPEG/PNG at much smaller sizes. Serving AVIF or WebP with a JPEG fallback is one of the highest-leverage single changes for LCP.
- **Serve responsive sizes.** Use `srcset` so phones get a small image and big screens get a big one, instead of everyone downloading the desktop version.
- **Don't lazy-load the LCP image.** Lazy-loading is great for images *below* the fold, but lazy-loading the hero delays the very element LCP is timing. Load that one eagerly, even prioritize it.

```text
   hero.jpg  (3000px, unoptimized JPEG)   1,800 kB   ← the LCP killer from Phase 2
        │  resize to displayed size (1200px)
        │  convert to AVIF
        ▼
   hero.avif (1200px)                       110 kB   ← ~16× smaller, same apparent quality
```

*What just happened:* The same hero went from 1.8 MB to roughly 110 kB by resizing it to the size it's actually shown at and switching to a modern format. On a slow connection that's the difference between a multi-second LCP and a fast one - and it's the single change most likely to move the needle on an image-heavy page.

## 3. Caching and a CDN

Two different "where do the bytes come from" wins, and they compound.

**Caching** tells the browser to keep a copy of a resource so the *next* visit doesn't re-download it. You do this with HTTP cache headers (`Cache-Control`). For files that never change - your hashed JS and CSS bundles, images - you can cache them for a long time. The user's second visit, and every page-to-page navigation, loads those from disk instead of the network: effectively zero load time.

**A CDN** (Content Delivery Network) puts copies of your static files on servers physically close to your users, all over the world. Instead of every byte traveling from your one origin server in, say, Virginia, a user in Tokyo gets it from a Tokyo edge node. Less distance means lower latency, which improves TTFB and LCP - and CDNs cache aggressively, taking load off your origin.

```text
   WITHOUT CDN                         WITH CDN
   user (Tokyo) ───────────────▶       user (Tokyo) ──▶ edge (Tokyo)  ⚡ fast
        origin (Virginia)                                   │ (only on a miss)
        ~150 ms each way                                     ▼
                                                        origin (Virginia)
```

*What just happened:* The CDN moves the bytes geographically closer, so most requests are answered by a nearby edge node in a few milliseconds instead of crossing an ocean. Caching and a CDN are the cheapest LCP/TTFB wins for a global audience precisely because, like all caching, they delete work rather than speed it up - the request that hits a warm edge cache barely touches your servers at all.

## 4. Kill render-blocking resources

When the browser parses your HTML and hits a `<link>` to a stylesheet or a synchronous `<script>` in the `<head>`, it can stop and wait - fetching and processing that resource *before* it paints anything. A few of these stacked in the head is a blank screen the user stares at. That's render-blocking, and it's a direct hit to LCP.

The fixes:

- **Defer non-critical JavaScript.** Add `defer` (or `async`) to scripts so the browser keeps parsing and painting instead of blocking on them. Most scripts don't need to run before first paint.
- **Inline the critical CSS, defer the rest.** Ship the small amount of CSS needed to render what's visible immediately, and load the big stylesheet without blocking.
- **Trim what loads in the head at all.** Every blocking resource is a gate between the user and the first paint. Move what you can out of the way.

```html
<!-- Render-blocking: browser waits for this before painting -->
<script src="analytics.js"></script>

<!-- Non-blocking: browser keeps painting, runs the script after -->
<script src="analytics.js" defer></script>
```

*What just happened:* Adding `defer` tells the browser it doesn't need this script before showing the page, so the script downloads in the background and runs after the document is parsed. Analytics, chat widgets, and most third-party scripts have no business blocking the first paint - defer them and LCP improves with no visible downside.

## 5. Size your media - the CLS fix

CLS comes almost entirely from elements that arrive without having reserved their space, so when they appear they shove everything else. The fix is to tell the browser how big they'll be *before* they load, so it holds the space open.

- **Always set `width` and `height` on images** (or a CSS `aspect-ratio`). The browser reserves a box of the right shape immediately, and the image fills it without pushing anything.
- **Reserve space for ads, embeds, and dynamic content.** Give the slot a fixed min-height so a late-arriving banner drops into held-open space instead of inserting itself.
- **Avoid inserting content above existing content.** A cookie banner or notification that pushes the page down after the user starts reading is a classic CLS spike.

```html
<!-- No reserved space: when this loads, everything below it JUMPS down -->
<img src="hero.avif" alt="...">

<!-- Reserved space: browser holds a 1200×600 box, nothing shifts -->
<img src="hero.avif" alt="..." width="1200" height="600">
```

*What just happened:* With explicit dimensions, the browser knows the image's aspect ratio before a single byte of it arrives, so it lays out the page correctly the first time and the image simply fills its waiting box. No jump, no CLS. This one attribute is the highest-leverage CLS fix there is, and it costs nothing.

## Recap

1. **Bundle size and code splitting** - JS is the most expensive byte; ship less and load the rest on demand. Moves INP and LCP.
2. **Images** - the hero is usually the LCP element; resize, compress, use AVIF/WebP, and don't lazy-load it. Moves LCP.
3. **Caching and a CDN** - cache immutable assets and serve from edges near the user; this deletes work rather than speeding it. Moves LCP and TTFB.
4. **Kill render-blocking resources** - `defer` non-critical scripts and tame head CSS so the browser paints sooner. Moves LCP.
5. **Size your media** - set width/height and reserve space so nothing jumps. Moves CLS.

The throughline is the one from the rest of performance: **measure first, pull the lever the measurement pointed at, then confirm the win in field data.** A page that loads its main content fast, holds still while it does, and responds the instant you touch it - that's not a vibe. It's three numbers you now know how to read and move.

```quiz
[
  {
    "q": "Why is JavaScript considered the most expensive kind of byte for performance, hurting both INP and LCP?",
    "choices": [
      "It's always the largest file on the page",
      "It must be downloaded, parsed, compiled, and executed on the main thread - the same thread that paints and handles clicks",
      "Browsers refuse to cache JavaScript",
      "It can't be compressed"
    ],
    "answer": 1,
    "explain": "Unlike an image that's merely decoded, JS occupies the main thread to parse, compile, and run. While it does, the browser can't paint (LCP) or respond to input (INP)."
  },
  {
    "q": "Your LCP element is the hero image. Which change is the LEAST helpful for LCP?",
    "choices": [
      "Resizing it to the size actually displayed and converting to AVIF",
      "Adding lazy-loading to the hero image",
      "Serving it from a CDN edge near the user",
      "Removing a render-blocking script in the head"
    ],
    "answer": 1,
    "explain": "Lazy-loading is for below-the-fold images. Lazy-loading the LCP element delays the very thing LCP measures - load the hero eagerly, even prioritize it."
  },
  {
    "q": "Adding width and height attributes to your images primarily improves which vital, and why?",
    "choices": [
      "INP, because it reduces JavaScript",
      "LCP, because the image downloads faster",
      "CLS, because the browser reserves the correct space so content doesn't jump when the image loads",
      "TTFB, because the server responds sooner"
    ],
    "answer": 2,
    "explain": "Knowing the dimensions up front lets the browser hold open a correctly-shaped box, so the image fills it without shoving anything - eliminating the layout shift that drives CLS."
  }
]
```
