DEV Community

Anosh
Anosh

Posted on

Core Web Vitals: What They Actually Mean Technically

"Core Web Vitals are important for SEO" is the kind of sentence that's true and useless at the same time. It tells you to care, but not what to open in your editor on Monday morning.

So let's skip the pep talk and look at what each metric measures, what causes it to go bad, and what a developer can actually change.

The three metrics, quickly

Core Web Vitals are three field metrics, measured on real users and judged at the 75th percentile of page loads:

  • LCP (Largest Contentful Paint): how long until the biggest visible element renders. Good is 2.5 seconds or less.
  • INP (Interaction to Next Paint): how quickly the page responds visually after a user interaction. Good is 200 milliseconds or less. It replaced FID in 2024.
  • CLS (Cumulative Layout Shift): how much visible content jumps around unexpectedly. Good is 0.1 or less.

One thing worth saying early: these are field numbers. Lighthouse in your own browser is a lab test on your fast laptop. Real users on mid-range phones and patchy connections are the ones Google measures, so use PageSpeed Insights (the CrUX data at the top) or Search Console to see what they experience.

LCP: why the biggest thing loads late

LCP is usually a hero image, a large heading, or a big text block. The element is whatever the browser decides is largest in the viewport.

The useful mental model is that LCP is a chain of four phases:

  1. Time to First Byte (TTFB): how long the server takes to respond
  2. Resource load delay: how long before the browser starts fetching the LCP resource
  3. Resource load duration: how long the download takes
  4. Element render delay: how long between "downloaded" and "painted"

When LCP is bad, one of those phases is almost always the culprit. Find which one before changing anything.

What causes slow LCP

  • Slow server response. If TTFB is already 1.5 seconds, you've spent most of your budget before the browser sees any HTML. Common causes are no caching, slow database queries, distant servers and no CDN.
  • Hero images that are too heavy. A 2 MB unoptimized JPEG as your banner is the classic offender.
  • Hero images discovered late. If the image is a CSS background or gets injected by JavaScript, the browser can't find it until much later.
  • Lazy-loading the LCP image. loading="lazy" on your hero tells the browser to deprioritize the exact thing it should hurry up.
  • Render-blocking CSS. The browser won't paint until it has the stylesheets in the <head>. A giant CSS bundle delays everything.
  • Render-blocking JavaScript. Synchronous scripts in the head pause HTML parsing.
  • Web fonts. If your LCP is a headline, it may sit invisible while the font file downloads.
  • Client-side rendering. If the page is an empty <div id="root"> until a JS bundle runs, the LCP element doesn't exist until all that work finishes.

What you can change

Fix the server first, if TTFB is the problem:

  • Cache HTML where you can (page caching, edge caching)
  • Put a CDN in front of the site
  • Optimize slow queries and heavy backend work
  • Avoid redirect chains before the page even loads

Make the hero image fast and discoverable:

<img
  src="/hero.avif"
  width="1200"
  height="600"
  fetchpriority="high"
  alt="Product hero image"
/>
Enter fullscreen mode Exit fullscreen mode
  • Serve modern formats like WebP or AVIF
  • Use responsive images (srcset and sizes) so phones don't download desktop-sized files
  • Add fetchpriority="high" to the LCP image
  • Never lazy-load it
  • Keep it in the HTML as a real <img>, not a CSS background

If it must be discovered early and can't be in plain HTML, preload it:

<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">
Enter fullscreen mode Exit fullscreen mode

Deal with render-blocking resources:

  • Inline the small amount of critical CSS needed for above-the-fold content
  • Load the rest of the CSS without blocking
  • Add defer (or async, where order doesn't matter) to scripts
  • Remove unused CSS and JS instead of just minifying them

Handle fonts sensibly:

  • Use font-display: swap so text shows immediately in a fallback
  • Preload your one or two critical font files
  • Self-host fonts instead of waiting on a third-party domain
  • Subset fonts so you're not shipping characters you never use

Consider rendering strategy. If you're on a JS framework, server-side rendering or static generation gets real HTML to the browser faster than shipping an empty shell.

INP: why the page feels sluggish

INP measures the delay between a click, tap or keypress and the next frame the browser paints in response. It looks at interactions across the whole visit and reports roughly the worst one, which is why one bad interaction can hurt you.

Each interaction has three parts:

  • Input delay: the wait before the handler can even start (the main thread is busy)
  • Processing time: how long your event handlers run
  • Presentation delay: the time to recalculate layout and paint the result

What causes poor INP

  • Long tasks. Any task that holds the main thread for more than 50 ms can block the browser from responding. Several in a row make a page feel frozen.
  • Main-thread blocking in general. The main thread handles JavaScript, style calculation, layout and painting. If JS hogs it, nothing else happens.
  • Heavy event handlers. A click handler that filters 5,000 items, updates a huge state tree and re-renders half the page will feel slow.
  • Third-party scripts. Chat widgets, tag managers, analytics, A/B testing tools and ad scripts all compete for the same main thread, and you don't control their code.
  • Large DOM size. Updating a page with tens of thousands of nodes costs more at every step.
  • Expensive rendering work. Forcing layout repeatedly (layout thrashing) or re-rendering too much UI after each interaction.

What you can change

Break up long tasks. Yield back to the main thread so the browser can respond to the user between chunks of work:

async function processItems(items) {
  for (const item of items) {
    heavyWork(item);
    // Let the browser handle pending input and paint
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}
Enter fullscreen mode Exit fullscreen mode

Newer APIs like scheduler.yield() do this more cleanly where supported, but the idea is the same: don't hold the thread for 300 ms in one go.

Keep event handlers light:

  • Do the minimum needed to show visual feedback first, then defer the heavy part
  • Debounce or throttle handlers for input, scroll and resize
  • Avoid synchronous work you don't need before the next paint

Move heavy work off the main thread. Web Workers are good for parsing, calculations and data crunching that doesn't touch the DOM.

Cut down the JavaScript you ship:

  • Code-split so each page loads only what it needs
  • Remove unused libraries (that date library you imported for one function)
  • Lazy-load features that appear only after interaction

Tame third-party scripts:

  • Audit every one and delete what nobody uses
  • Load non-essential scripts after the page is interactive, or on user interaction
  • Prefer lighter alternatives where they exist
  • Measure the impact of each one, because the cost is often a surprise

Reduce rendering cost:

  • Keep the DOM small and avoid deeply nested structures
  • Virtualize very long lists
  • Batch DOM reads and writes to avoid layout thrashing
  • In React-style frameworks, avoid unnecessary re-renders with sensible state placement and memoization

CLS: why things jump around

CLS adds up unexpected layout shifts. A shift happens when a visible element moves between frames without the user causing it. The score combines how much of the viewport moved and how far it moved.

The painful version is familiar: you go to tap a button, an ad loads above it, everything slides down, and you tap the wrong thing.

What causes layout shifts

  • Images and videos without dimensions. The browser doesn't know how much space to reserve, so content below gets pushed when the media loads.
  • Ads and embeds. Ad slots that resize after loading, or iframes (maps, videos, social posts) with no reserved space.
  • Dynamic content. Banners, cookie notices, "related items" blocks or personalized content inserted above existing content.
  • Web fonts. When the fallback font swaps to the real one, text can reflow if the two have different metrics.
  • Injected elements. Anything added to the DOM above content the user is already looking at, like a promo bar sliding in after load.
  • Late-loading CSS. Styles that arrive after first paint can change the layout.

Note that shifts caused directly by user input (like clicking to expand an accordion) are excluded. It's the unexpected ones that count.

What you can change

Always give media dimensions:

<img src="/photo.webp" width="800" height="450" alt="..." />
Enter fullscreen mode Exit fullscreen mode

The browser uses the width and height to calculate the aspect ratio and reserve space before the file arrives. You can also set aspect-ratio in CSS:

.video-wrapper {
  aspect-ratio: 16 / 9;
}
Enter fullscreen mode Exit fullscreen mode

Reserve space for ads and embeds:

  • Give ad slots a min-height that matches the most common ad size
  • Put placeholders around iframes and embedded widgets
  • Avoid inserting ads above content that's already in view

Be careful with dynamic content:

  • Insert new content below the viewport or in space you've already reserved
  • For banners and cookie notices, use fixed or overlay positioning, or reserve their height from the start
  • Show skeleton placeholders that match the final size of the content

Reduce font-swap shifts:

  • Preload critical fonts
  • Use font-display: optional or swap depending on how much a brief flash matters to you
  • Match the fallback font's metrics to the web font with size-adjust, ascent-override and similar descriptors, so the swap barely changes the layout

Animate with transforms. Animations that change top, height or margin trigger layout and can cause shifts. transform and opacity don't.

How to find what's actually wrong

Fixing things blindly is how teams waste a sprint. A better workflow:

  1. Start with field data. Check Search Console's Core Web Vitals report and the CrUX section in PageSpeed Insights to see which metric fails and on which pages.
  2. Reproduce in the lab. Use Lighthouse and the Chrome DevTools Performance panel, ideally with CPU and network throttling to mimic a mid-range phone.
  3. Find the specific cause.
    • For LCP, look at which element it is and which of the four phases is longest
    • For INP, record an interaction in the Performance panel and look for long tasks
    • For CLS, use the Layout Shift regions in DevTools to see what moved
  4. Fix one thing, then measure again.
  5. Wait for field data. CrUX uses a rolling 28-day window, so improvements take a few weeks to show in your real-user numbers.

Quick reference

  • LCP too high: slow server, heavy or late-discovered hero image, render-blocking CSS and JS, fonts, client-side rendering. Fix with caching, image optimization, fetchpriority, preload, deferred scripts and SSR.
  • INP too high: long tasks, heavy handlers, third-party scripts, big DOM. Fix by splitting work, using workers, trimming JS and auditing third parties.
  • CLS too high: media without dimensions, ads, injected content, font swaps. Fix with explicit sizes, reserved space, stable fonts and transform-based animation.

Final thoughts

Core Web Vitals look intimidating until you notice that each one is really a simple complaint from a real user:

  • LCP: "this is taking forever to show up"
  • INP: "I tapped it and nothing happened"
  • CLS: "everything moved right as I tried to tap"

Every technical fix above comes from taking one of those complaints seriously. And it's worth remembering that these metrics are one signal among many. A fast page won't rescue weak content, but a slow, jumpy one makes everything else harder.

If you've got a Core Web Vitals problem that refuses to go away, share it in the comments. The stubborn ones are usually the most educational.

Also check out my website : anoshbb.com

Suggested tags: webdev, performance, seo, javascript

Top comments (0)