Web Performance Optimisation: Core Web Vitals 2026
TL;DR
- INP (Interaction to Next Paint) replaced FID in 2024. It measures total interaction responsiveness, not just first input delay
- LCP is still dominated by unoptimised images. Use
<link rel="preload">, responsivesrcset, and modern formats (WebP/AVIF)- CLS problems in 2026 are mostly caused by dynamic content insertion, web fonts, and third-party ad scripts
- Google measures field data from real users (CrUX), not your Lighthouse lab score. Optimise for the 75th percentile of real user experience
Table of Contents
- INP Replacing FID
- LCP Optimisation Strategies
- CLS: Still Catching Teams Out
- What Google Actually Measures
- Next.js-Specific Optimisations
- Critical Rendering Path
- JavaScript Bundle Optimisation
- Third-Party Scripts: The Silent Killer
- Frequently Asked Questions
INP Replacing FID
First Input Delay (FID) was retired as a Core Web Vital in March 2024, replaced by Interaction to Next Paint (INP).
FID measured: How long the browser takes to start processing the first user interaction (click, tap, key press). It only measured the delay before processing started, not the time to show the visual result. Most sites passed FID easily because it only counted the first interaction.
INP measures: The total time from user interaction to the next visual update, across ALL interactions throughout the page lifecycle. It is the 98th percentile of interaction latency for the entire session.
This means INP catches:
- Slow event handlers that block the main thread
- Heavy re-renders triggered by state changes
- Layout recalculation after DOM mutations
- Navigation transitions that freeze the UI
How to Improve INP
- Break up long tasks: Use
requestIdleCallbackorscheduler.yield()to break JavaScript execution into smaller chunks - Debounce expensive handlers: Search inputs, filter changes, and resize handlers should be debounced to avoid processing every keystroke
- Use
startTransitionin React: Mark non-urgent updates as transitions so they do not block user interactions - Move computation off the main thread: Use Web Workers for JSON parsing, data sorting, or complex filtering
// Bad: blocks main thread during search
function handleSearch(query: string) {
const results = allItems.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
); // Blocks for 50ms+ with 10,000 items
setResults(results);
}
// Good: uses startTransition for non-urgent update
function handleSearch(query: string) {
setQuery(query); // Urgent: update the input immediately
startTransition(() => {
const results = allItems.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
setResults(results); // Non-urgent: can be interrupted
});
}
LCP Optimisation Strategies
Largest Contentful Paint measures when the largest visible element finishes rendering. For most pages, this is a hero image, header text block, or video poster.
Image Optimisation (Most Common LCP Fix)
<!-- Preload the hero image so it starts loading before the CSS/JS -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- Responsive image with modern formats -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg"
alt="Product showcase"
width="1200"
height="630"
fetchpriority="high"
loading="eager">
</picture>
Key principles:
- Preload the LCP image: Add a
<link rel="preload">in the document<head>so the browser fetches it immediately - Use
fetchpriority="high": Tells the browser this image is critical - Serve modern formats: AVIF is 30-50% smaller than WebP, which is 25-35% smaller than JPEG
- Set explicit dimensions: Prevents CLS by reserving the correct space before the image loads
- Never lazy-load the LCP element: Only lazy-load below-the-fold images
Server-Side Rendering for Text-Based LCP
If your LCP element is text (a heading or paragraph), SSR ensures it appears in the initial HTML response without waiting for JavaScript:
// Next.js: Server Component renders text immediately in HTML
export default async function HeroSection() {
return (
<h1 className="text-5xl font-bold">
Build Better Apps Faster
</h1>
);
}
CLS: Still Catching Teams Out
Cumulative Layout Shift measures unexpected visual movement during page load. Good score: under 0.1. Bad score: above 0.25.
Common CLS Causes in 2026
Dynamic content insertion: A cookie consent banner, newsletter popup, or notification bar pushes page content down after it loads. Fix: reserve space for dynamic elements using CSS min-height or render them as overlays instead of pushing content.
Web fonts: When a web font loads and replaces the fallback font, text can reflow, changing line breaks and element heights. Fix:
/* Use font-display: optional for non-critical fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: optional; /* Falls back silently if font is slow */
}
/* Or use size-adjust to match fallback metrics */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 25%;
line-gap-override: 0%;
}
Ads (the biggest CLS offender): Third-party ad scripts insert iframes of unpredictable sizes. Fix:
/* Reserve exact space for ad slots */
.ad-container {
min-height: 250px; /* Match the ad unit's expected height */
width: 300px;
}
Images without dimensions: Always include width and height attributes on <img> tags. The browser uses these to calculate aspect ratio and reserve space before the image loads.
What Google Actually Measures
Lab data (Lighthouse): Simulated conditions on a specific device and network configuration. Useful for development and catching regressions but NOT what Google uses for ranking.
Field data (Chrome User Experience Report / CrUX): Real user measurements collected from Chrome users who opt into usage statistics. Google uses the 75th percentile of field data for ranking decisions.
This means:
- Your Lighthouse score of 95 does not matter if real users on slow devices get poor scores
- Performance varies dramatically by device, network, and geography
- Optimise for the 75th percentile: the experience must be good for 75% of your users, not just those on fast connections
Access your CrUX data via PageSpeed Insights, the CrUX API, or the CrUX Dashboard in BigQuery.
Next.js-Specific Optimisations
Next.js provides built-in performance features that directly impact Core Web Vitals:
Image Component:
import Image from 'next/image';
<Image
src="/hero.webp"
alt="Product showcase"
width={1200}
height={630}
priority // Disables lazy loading, adds preload hint
sizes="(max-width: 768px) 100vw, 1200px"
/>
Font Optimisation (next/font):
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
});
Next.js automatically inlines the font CSS, eliminating the extra network request that causes CLS.
Streaming SSR with Suspense reduces TTFB by sending HTML progressively:
export default function Page() {
return (
<main>
<HeroSection /> {/* Sent immediately */}
<Suspense fallback={<ProductSkeleton />}>
<ProductList /> {/* Streamed when data is ready */}
</Suspense>
</main>
);
}
Critical Rendering Path
The critical rendering path determines how quickly the browser can paint the first frame:
- HTML parsing: Browser reads the HTML document
- CSS loading and parsing: Render-blocking by default
- JavaScript loading and execution: Parser-blocking if in
<head>withoutdefer/async - Layout calculation: Browser determines element sizes and positions
- Paint: Browser draws pixels
To speed up the critical path:
- Inline critical CSS (above-the-fold styles) in
<head> - Load non-critical CSS with
media="print" onload="this.media='all'" - Use
deferfor all non-critical JavaScript - Avoid synchronous third-party scripts in
<head>
JavaScript Bundle Optimisation
Code splitting divides your JavaScript into chunks that load on demand:
// Next.js: dynamic import loads component only when needed
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Skip SSR for client-only components
});
Tree shaking eliminates unused code from your bundle. Ensure you:
- Use ES module imports (
import { specific } from 'lib') not CommonJS (require('lib')) - Check that third-party libraries support tree shaking (look for
"sideEffects": falsein their package.json) - Review your bundle with
@next/bundle-analyzerto find large unused dependencies
Third-Party Scripts: The Silent Killer
Third-party scripts (analytics, ads, chat widgets, social embeds) are the most common uncontrolled performance drain:
| Script Type | Typical Impact | Mitigation |
|---|---|---|
| Analytics (GA4) | +50-100ms TBT | Load with afterInteractive strategy |
| Ad scripts | +200-500ms TBT, CLS | Reserve space, lazy-load below fold |
| Chat widgets | +100-300ms TBT | Load after user scroll or delay 5s |
| Social embeds | +200ms per embed | Use facade pattern (static placeholder until click) |
For testing how your API response times affect page performance, use our free API Request Tester to measure endpoint latency. Our JSON formatter helps audit response payload sizes.
The Debuggers provides web performance optimisation services, helping teams achieve green Core Web Vitals scores across all pages.
For related web development topics, see our Next.js 15 App Router guide and our React Server Components guide.
Frequently Asked Questions
Does Lighthouse score affect Google ranking?
Lighthouse score itself does not directly affect ranking. Google uses field data from the Chrome User Experience Report (CrUX), not lab data from Lighthouse. However, Lighthouse identifies the same issues that affect real user experience, so improving your Lighthouse score typically improves your CrUX data. Focus on field data for ranking impact and use Lighthouse as a diagnostic tool to find issues.
What is a good INP score?
Google considers INP under 200ms as "good," 200-500ms as "needs improvement," and above 500ms as "poor." Most sites struggle with INP more than they did with FID because INP measures all interactions throughout the session, not just the first one. Complex single-page applications with heavy client-side rendering are most affected. Server-rendered pages with minimal client-side JavaScript typically pass INP easily.
How do I fix CLS caused by ads?
Reserve explicit space for every ad slot using CSS min-height and width values that match the expected ad size. If the ad size varies, use the largest expected size. For ad slots that may not fill, set a neutral background colour so the reserved space does not look broken. Load ad scripts with defer or async to prevent them from blocking the initial render. Consider lazy-loading ad slots below the fold.
Is Core Web Vitals still a ranking factor in 2026?
Yes. Google confirmed that page experience signals, including Core Web Vitals, remain a ranking factor. However, content relevance and quality are still the primary ranking signals. Core Web Vitals act as a tiebreaker: among pages with similar content quality and relevance, Google favours those with better user experience. Meeting the "good" threshold is more important than achieving a perfect score because the ranking impact plateaus once you pass the threshold.
Testing your site's API performance?
Use our free API Request Tester to measure endpoint response times. Slow APIs directly impact LCP and INP scores.
Need help optimising your website's performance? The Debuggers provides web performance consulting for teams targeting Core Web Vitals compliance.
Found this helpful?
Join thousands of developers using our tools to write better code, faster.