Skip to main content

Home / blog / what-breaks-core-web-vitals-2026

What actually breaks Core Web Vitals in 2026 — and how to find it

Published · 9 min read · RankVitals engineering

The thresholds have not moved in years: Largest Contentful Paint under 2.5 seconds, Cumulative Layout Shift under 0.1, and Interaction to Next Paint under 200 ms — all measured at the 75th percentile of real users. In the lab, Total Blocking Time stands in for interactivity, since INP needs real interactions to exist. What has changed is tooling: sites in 2026 ship more JavaScript, more third-party tags, and more dynamically injected content than ever, and the same six failure classes account for the overwhelming majority of failing reports we see.

This is a walk through those six classes as our test engine actually detects them. For each one: what the metric measures, how the problem shows up in a report, the exact fix, and the specific audit or check id that flags it — so you can go from "the score is orange" to a named file in one step.

1. Render-blocking CSS and JavaScript

LCP measures the time until the largest text block or image in the viewport finishes painting. Nothing can paint until the browser has built the render tree, and every stylesheet in <head> — plus every classic <script> without defer or async — blocks that. Each blocking file costs at least one network round trip, and on a throttled mobile connection round trips are 150 ms or more. Four blocking files can push First Contentful Paint past a second before a single byte of your HTML is rendered.

In a report this looks like a long silent gap at the start of the filmstrip: white frames while CSS downloads, then everything appears at once. The Lighthouse audit render-blocking-resources catches it and estimates the savings per file; in RankVitals it lands in the Structure tab with the offending URLs attached.

The fix is to make the critical path tiny: inline the CSS needed for the first viewport, load the rest asynchronously, mark scripts defer, and warm up third-party origins you cannot avoid.

<!-- critical CSS inlined in <head>; the rest loads without blocking -->
<link rel="preload" href="/css/site.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">

<!-- scripts: never bare <script src>, always defer (or async for beacons) -->
<script src="/js/app.js" defer></script>

<!-- third-party origin you will definitely hit: pay the handshake early -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

2. Oversized and legacy-format images

The LCP element on most pages is the hero image, so image weight converts directly into LCP milliseconds. The three recurring variants: images encoded as PNG or JPEG when WebP/AVIF would be 30–70% smaller, images served at 2400px width into a 800px slot, and images compressed at quality settings nobody ever revisited. Lighthouse splits these across modern-image-formats, uses-responsive-images and uses-optimized-images, each with a byte-savings estimate per file. Our SEO crawler independently flags the format problem as image_legacy_formats when a PNG/JPEG has no WebP or AVIF alternative in its srcset or <picture> sources — so it gets caught even on pages you only audit for SEO.

The durable fix is a <picture> element with modern formats first and the JPEG as fallback — the browser picks the first type it supports:

<picture>
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" width="1200" height="630"
       fetchpriority="high" alt="Product dashboard">
</picture>

On Next.js, next/image does all of this — format negotiation, resizing, lazy loading — in one component. The one thing it cannot guess is which image is the LCP element: mark that one priority so it is preloaded instead of lazy-loaded. Our Next.js speed guide covers the rest of the framework-specific defaults worth changing.

import Image from 'next/image';

<Image src="/img/hero.jpg" width={1200} height={630}
       priority alt="Product dashboard" />

3. Eagerly loading images nobody can see

Every image below the fold that loads eagerly competes for bandwidth with the hero image and the critical CSS — on a constrained connection that contention is measurable LCP delay. Lighthouse reports it as offscreen-images with the deferred-bytes estimate; our crawler applies a blunter heuristic (image_lazy_loading): any image beyond the first few in document order without loading="lazy" is probably offscreen at load time and gets flagged.

The fix is one attribute — plus explicit dimensions, which you will want anyway for section 6. Do not lazy-load the LCP image itself; that is the one image that should load as early as possible.

<img src="/img/team-photo.webp" width="800" height="533"
     loading="lazy" decoding="async" alt="The team at the 2025 offsite">

4. Cache lifetimes measured in minutes

Caching does not change a first-visit lab score much, which is exactly why it gets neglected — but field data is dominated by repeat visitors, and a site that re-downloads 2 MB of unchanged JavaScript on every visit fails LCP for the users who come back. The uses-long-cache-ttl audit totals the bytes served with short or missing Cache-Control headers; seeing your own hashed bundle in that list means every visitor pays full price every time.

The pattern that fixes it: fingerprint static asset filenames at build time (every modern bundler does), then cache them for a year as immutable. HTML stays short-lived so deploys propagate.

# hashed static assets: safe to cache forever — a deploy changes the filename
location /assets/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML: always revalidate so deploys show up immediately
location / {
  add_header Cache-Control "no-cache";
}

5. Third-party scripts and the main thread

Total Blocking Time sums every main-thread task longer than 50 ms between First Contentful Paint and Time to Interactive — counting only the portion past 50 ms, the part where the page cannot respond to input. It is the lab stand-in for INP, and in 2026 it is overwhelmingly a third-party problem: tag managers loading tag managers, chat widgets, session replay, consent platforms, and A/B testing scripts that block rendering by design. Lighthouse attributes the damage per origin in third-party-summary, with bootup-time and mainthread-work-breakdown showing where your own bundles contribute.

There is no clever fix, only an honest inventory. In rough order of effectiveness:

  • Delete tags nobody can name an owner for — in our experience most sites with 15+ third parties have several orphans.
  • Replace heavy embeds (YouTube, maps, chat) with facades: a static image that loads the real widget on interaction.
  • Load analytics and marketing tags after the page is interactive, not before.
  • Self-host what you can — fonts especially — to cut connection setup to extra origins.
  • Re-test after each removal: TBT drops in direct proportion, which makes a compelling artifact for the "we need this tag" conversation.

6. Layout shift from unsized media

CLS measures visual instability: each unexpected shift scores impact-fraction × distance-fraction, summed over the worst burst of shifts. The 0.1 threshold is easy to blow with a single late-loading element pushing content down — an image without dimensions, an ad slot that expands from zero height, a web font swapping in at a different size. Users experience it as the tap-the-wrong-button page.

The unsized-images audit lists every <img> lacking width and height attributes; the layout-shift diagnostics point at the specific shifting nodes. When an image has dimensions, the browser reserves the box before a single byte arrives — the layout simply never moves. The same reservation trick works for anything of unknown height: give the container a min-height or aspect-ratio and let content fill it.

<!-- width/height set the aspect ratio; CSS keeps it responsive -->
<img src="/img/chart.webp" width="1200" height="800" alt="Latency chart"
     style="max-width: 100%; height: auto;">

<!-- reserve space for late content (ads, embeds, hydrating widgets) -->
<div style="min-height: 280px;"><!-- ad slot --></div>

Lab vs field: why the two numbers disagree

Lab data (Lighthouse) loads your page once, in a controlled environment, with simulated throttling — observed network activity and CPU time are replayed against a modelled connection (in our default profile: a set RTT, capped throughput, and a CPU slowdown multiplier). Field data (CrUX) is the 75th-percentile experience of your actual Chrome users over the trailing 28 days. They disagree constantly, and both are telling the truth.

Lab flatters you when your real users are on slower devices and networks than the simulation, and when problems only appear at scale — cold CDN edges, personalization backends. Lab damns you unfairly when your audience is on fast desktop connections, since the simulated mobile profile is deliberately pessimistic. And two things exist only in the field: INP, which needs real interactions, and post-load CLS from content that shifts when users scroll — lab CLS only observes the load. The practical workflow: field data decides whether you have a problem, lab data tells you which file caused it. You need both; treat the lab as a debugger, not a verdict.

How we rank what to fix first

A Lighthouse run emits dozens of audits, and a flat list is where good intentions die. Our engine classifies every failing audit into an impact bucket using its score together with the estimated savings in milliseconds and bytes, then weights those buckets into a Structure score. The overall Grade mirrors the formula GTmetrix made familiar: 70% Lighthouse Performance, 30% Structure — so the letter tracks both how fast the page was and how much fixable headroom is left (how that compares to GTmetrix itself). Failing audits carry their top offending resources — the actual URLs with wasted ms and bytes per file — so the report says "this 940 KB PNG on your homepage", not "consider optimizing images". If you only want the three field metrics without a full run, the Core Web Vitals checker reads them straight from CrUX. Run a free test below and see which of the six classes above your site is paying for.

FAQ

Is TBT the same as INP?
No. INP measures real user interactions and only exists in field data; TBT is the lab proxy, measuring how long the main thread was too busy to respond during load. They correlate well — a page with high TBT almost always has poor INP — but you can have good TBT and still fail INP if slow event handlers fire on interaction, long after load.
My Lighthouse score is 90+ but Search Console says my vitals fail. Why?
Search Console uses CrUX field data: the 75th percentile of your real users over 28 days. If your audience skews toward slower devices or networks than the lab simulation, field numbers will be worse than lab numbers. Fix what the field data flags, use lab runs to find the responsible resources, then wait out the 28-day window.
Do I need to fix every failing audit?
No. Audits are ranked by estimated impact for a reason — the top two or three usually account for most of the recoverable time. Fix those, re-test, and re-rank. Chasing every last diagnostic is how performance work loses momentum without moving the p75.

Test your site now

Free Lighthouse speed test with waterfall + filmstrip, and an SEO audit in the same tool.

Enter a public website address, including https://. No account is needed to start a trial test.