
Core Web Vitals Optimization: A Practical Guide for 2026
Core Web Vitals are Google’s official set of metrics that measure real-world user experience on the web. They directly influence search rankings as part of Google’s Page Experience signals. Yet most guides stop at explaining what they are. This guide goes further — it tells you exactly how to fix them, with real code examples and SvelteKit-specific techniques.
What Are Core Web Vitals?
Core Web Vitals are a subset of Web Vitals, a Google initiative that provides unified guidance for quality signals essential to delivering a great user experience on the web. Unlike synthetic lab metrics, Core Web Vitals are measured in the field — from real users on real devices.
As of 2026, there are three Core Web Vitals:
| Metric | What It Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Loading performance | < 2.5s | 2.5s – 4.0s | > 4.0s |
| INP | Interactivity | < 200ms | 200ms – 500ms | > 500ms |
| CLS | Visual stability | < 0.1 | 0.1 – 0.25 | > 0.25 |
A page “passes” Core Web Vitals when 75% of real-world visits hit the “Good” threshold for all three metrics. This 75th-percentile requirement is intentional — it ensures you’re optimizing for most of your users, not just ideal conditions.
Largest Contentful Paint (LCP) — Loading Performance
What LCP Measures
LCP marks the point in the page load timeline when the largest content element in the viewport has finished rendering. This is typically a hero image, a large heading, or a video poster.
LCP is the most user-visible of the three metrics. A slow LCP means the user is staring at a blank or partially loaded page. Google’s research shows that sites with LCP under 2.5 seconds have 24% lower bounce rates than slower counterparts.
Common Causes of Poor LCP
- Unoptimized hero images (no WebP/AVIF, no preloading)
- Render-blocking JavaScript and CSS
- Slow Time to First Byte (TTFB) from the server
- Client-side rendering without a preloaded state
- Web fonts blocking text rendering
How to Fix LCP
1. Preload the LCP image
The single highest-impact change for image-heavy pages. Add a <link rel="preload"> in your <head> for the hero image so the browser discovers it immediately — before parsing the page body.
<link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high" /> 2. Use modern image formats
WebP cuts file sizes by 25–35% over JPEG. AVIF cuts them by 50% over JPEG. Serve the smallest format the browser supports using the <picture> element.
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" width="1200" height="630" fetchpriority="high" />
</picture> 3. SvelteKit: Use enhanced:img for automatic optimization
SvelteKit’s @sveltejs/enhanced-img automatically generates WebP/AVIF variants, adds srcset for responsive images, and preserves aspect ratios — all at build time.
<script>
import heroImage from "$assets/images/hero.png?enhanced";
</script>
<enhanced:img src={heroImage} alt="Hero" sizes="(min-width: 1024px) 65vw, 100vw" fetchpriority="high" loading="eager" /> 4. Eliminate render-blocking resources
Every <link rel="stylesheet"> and synchronous <script> in <head> delays LCP. Audit with Chrome DevTools Coverage panel and defer or async-load anything not needed for initial render.
<!-- Before -->
<script src="/analytics.js"></script>
<!-- After -->
<script src="/analytics.js" defer></script> 5. Reduce TTFB
A slow server is the root cause of poor LCP that no frontend optimization can fully compensate for. Use a CDN, enable HTTP/3, and implement proper caching headers.
Cache-Control: public, max-age=31536000, immutable ← For hashed assets
Cache-Control: public, max-age=3600, stale-while-revalidate=86400 ← For pages Interaction to Next Paint (INP) — Interactivity
What INP Measures
INP replaced First Input Delay (FID) as a Core Web Vital in March 2024. While FID only measured the first interaction, INP measures the worst interaction latency across the entire page visit.
INP captures the time from when a user interacts (clicks, taps, key presses) to when the browser paints the next frame in response. The threshold is strict: under 200ms is “Good.”
Important: INP is a 98th-percentile metric. One slow interaction in a session can hurt your score.
Common Causes of Poor INP
- Long JavaScript tasks blocking the main thread
- Inefficient event handlers that do too much work synchronously
- Excessive DOM size (> 1,500 nodes)
- Unoptimized third-party scripts (analytics, chat widgets, ads)
- React/Svelte re-renders touching too many components at once
How to Fix INP
1. Break up long tasks
Any JavaScript task that runs longer than 50ms on the main thread is a “long task” and will hurt INP. Use scheduler.yield() (or setTimeout(0) as a fallback) to yield control back to the browser between chunks of work.
async function processItems(items) {
for (const item of items) {
process(item);
// Yield to browser after each item to keep the main thread responsive
if ("scheduler" in window && "yield" in scheduler) {
await scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
} 2. Move heavy computation off the main thread
Web Workers run in a separate thread and cannot block the UI. Move CPU-intensive tasks — sorting, parsing, cryptography — to a worker.
// main.js
const worker = new Worker("/heavy-worker.js");
worker.postMessage({ data: largeDataset });
worker.onmessage = ({ data }) => updateUI(data.result);
// heavy-worker.js
self.onmessage = ({ data }) => {
const result = expensiveComputation(data.data);
self.postMessage({ result });
}; 3. Debounce and throttle event handlers
Input, scroll, and resize events fire many times per second. Debouncing ensures your handler runs only after the user stops.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const handleSearch = debounce((query) => fetchResults(query), 300); 4. SvelteKit: Avoid cascading reactive updates
In Svelte 5, reactive state updates are batched by the runtime. But deeply nested $derived chains that trigger expensive DOM mutations can still cause INP issues. Profile with Chrome DevTools Performance panel to identify which components are re-rendering unnecessarily.
<script>
// Expensive: runs on every keystroke
let query = $state("");
let results = $derived(expensiveSearch(query));
// Better: debounce the state update
let query = $state("");
let debouncedQuery = $state("");
let results = $derived(search(debouncedQuery));
const updateQuery = debounce((val) => {
debouncedQuery = val;
}, 300);
</script>
<input oninput={(e) => updateQuery(e.target.value)} /> Cumulative Layout Shift (CLS) — Visual Stability
What CLS Measures
CLS measures the total of all unexpected layout shift scores during the entire lifespan of a page. A layout shift occurs when a visible element moves from one rendered frame to the next — without user interaction triggering it.
The CLS score is calculated as: impact fraction × distance fraction for each shift, summed over the page visit. A score under 0.1 is “Good.”
Users find layout shifts deeply frustrating — they can cause mis-clicks, lost scroll position, and a general sense that the page is broken.
Common Causes of Poor CLS
- Images or iframes without
widthandheightattributes - Dynamically injected banners, cookie notices, or ads above existing content
- Web fonts causing a Flash of Unstyled Text (FOUT) that changes text dimensions
- Animations using
top,left,margin, or other layout-triggering properties
How to Fix CLS
1. Always declare image dimensions
The browser needs to know the aspect ratio before the image loads to reserve the correct space. Add explicit width and height attributes — CSS aspect-ratio takes care of the visual scaling.
<!-- Causes layout shift: browser doesn't know the height -->
<img src="photo.jpg" alt="Photo" />
<!-- No layout shift: browser reserves correct space -->
<img src="photo.jpg" alt="Photo" width="800" height="450" /> img {
width: 100%;
height: auto; /* maintains aspect ratio set by width/height attributes */
} 2. Use font-display: swap carefully
font-display: swap prevents invisible text during font loading but can cause layout shift when the fallback font is replaced by the web font (if they have different metrics). Use size-adjust, ascent-override, and descent-override to match fallback metrics.
@font-face {
font-family: "Kumbh Sans";
src: url("/fonts/kumbh-sans.woff2") format("woff2");
font-display: optional; /* No FOUT, no CLS — user sees cached font or system font */
} font-display: optional is often the best choice for CLS — it only uses the web font if it’s already in cache.
3. Reserve space for dynamic content
If you must inject content above the fold (cookie banners, promotional bars), reserve space for it in CSS before it loads so its appearance doesn’t push content down.
.cookie-banner-placeholder {
min-height: 60px; /* Reserve space before banner loads */
} 4. Use transform instead of layout-triggering properties for animations
transform and opacity are the only CSS properties that don’t trigger layout recalculation. Anything else (top, left, margin, width, height) causes layout thrashing and contributes to CLS.
/* Causes layout shift + poor performance */
.toast {
transition: margin-top 300ms;
}
/* No layout shift, GPU-accelerated */
.toast {
transition: transform 300ms;
transform: translateY(-100%);
}
.toast.visible {
transform: translateY(0);
} How to Measure Core Web Vitals
You need both lab data (synthetic, reproducible) and field data (real users, real devices) to get a complete picture.
Field Data Tools
- Google Search Console — Core Web Vitals report shows 28-day averages from your real users. This is the data Google uses for ranking.
- Chrome User Experience Report (CrUX) — Public dataset of real-user performance data. Query via PageSpeed Insights API or BigQuery.
web-vitalsJavaScript library — Measure LCP, INP, and CLS in your own analytics.
import { onLCP, onINP, onCLS } from "web-vitals";
onLCP(({ value, rating }) => {
console.log(`LCP: ${value}ms — ${rating}`);
// Send to your analytics endpoint
sendToAnalytics({ metric: "LCP", value, rating });
});
onINP(({ value, rating }) => sendToAnalytics({ metric: "INP", value, rating }));
onCLS(({ value, rating }) => sendToAnalytics({ metric: "CLS", value, rating })); Lab Data Tools
- Lighthouse — Built into Chrome DevTools. Run from the “Lighthouse” tab or via CLI (
npx lighthouse https://yoursite.com). Provides LCP and CLS (INP is field-only). - PageSpeed Insights — Combines Lighthouse lab data with CrUX field data in one report.
- WebPageTest — Advanced synthetic testing with filmstrips, waterfall charts, and multi-location testing.
- Chrome DevTools Performance Panel — The most detailed tool for diagnosing long tasks (INP) and layout shifts (CLS).
SvelteKit-Specific Performance Checklist
SvelteKit gives you powerful primitives for performance. Here’s a focused checklist:
LCP
- Use
<enhanced:img>for all content images — auto-generates WebP/AVIF, responsivesrcset - Add
fetchpriority="high"andloading="eager"to the LCP image - Use
<link rel="preload">in+layout.svelteorapp.htmlfor above-the-fold fonts - Enable prerendering (
export const prerender = true) wherever possible to eliminate TTFB
INP
- Audit
$derivedchains — avoid running expensive functions inside derived state - Use SvelteKit’s built-in code splitting — routes are split automatically
- Load analytics scripts with
deferin+layout.svelte
CLS
- Pass explicit
widthandheightto<enhanced:img>— the component preserves aspect ratio - Avoid injecting components into existing layouts after mount
- Use CSS
min-heighton containers that receive async data
Quick Reference: Core Web Vitals Thresholds
| Metric | Good | Needs Improvement | Poor | Percentile Used |
|---|---|---|---|---|
| LCP | ≤ 2.5s | 2.5s – 4.0s | > 4.0s | 75th |
| INP | ≤ 200ms | 200ms – 500ms | > 500ms | 75th (98th internally) |
| CLS | ≤ 0.1 | 0.1 – 0.25 | > 0.25 | 75th |
A page passes Core Web Vitals when 75% of real visits hit “Good” for all three metrics simultaneously.
Frequently Asked Questions
Do Core Web Vitals affect Google search rankings?
Yes. Google confirmed Core Web Vitals as a ranking signal in the Page Experience update (2021). They are one factor among hundreds, but for competitive niches where content quality is similar across top results, they can be the tiebreaker. Google uses field data from CrUX — not your Lighthouse score.
What is a good Core Web Vitals score?
All three metrics must be in the “Good” range for 75% of real-world visits. There is no single combined “score” — you need to pass each metric individually. PageSpeed Insights shows your pass/fail status at the top of the report.
How often does Google update Core Web Vitals metrics?
Google reviews the Core Web Vitals metric set annually. INP replaced FID in March 2024. Future additions are expected but typically announced 6–12 months in advance with a transition period.
Can I improve Core Web Vitals without changing my code?
To a degree. Moving to a faster CDN and enabling HTTP/3 can improve LCP. Removing unused third-party scripts (chat widgets, ad networks) can improve both LCP and INP. But the most impactful optimizations require code changes.
Is LCP the same as page load time?
No. LCP measures when the largest visible element renders, not when all resources have loaded (window.onload). A page can have a DOMContentLoaded time of 5 seconds but an LCP of 1.2 seconds if the hero element renders early. They measure fundamentally different things.
What replaced First Input Delay (FID)?
Interaction to Next Paint (INP) replaced FID as a Core Web Vital in March 2024. FID only captured the delay of the first interaction. INP captures all interactions throughout the session, making it a much more comprehensive measure of interactivity.
How do I measure INP in the field?
Use the web-vitals JavaScript library (onINP()) and send data to your analytics platform. The attribution object tells you exactly which element was interacted with and what caused the delay. Google Analytics 4 automatically collects INP if you use it.
Conclusion
Core Web Vitals are not a checkbox — they’re a continuous practice. The websites that consistently score well share a common approach: they measure in the field (not just in Lighthouse), they prioritize the 75th percentile of users, and they treat performance as a product requirement rather than an afterthought.
Start with the metric that’s furthest from “Good” in your Search Console report. Fix the highest-impact issues first (image optimization for LCP, third-party script removal for INP, explicit image dimensions for CLS). Measure again. Repeat.
Every millisecond you save is a user who doesn’t bounce.