1. Core Web Vitals Explained (LCP, INP, CLS)
Core Web Vitals are a set of real-world user experience metrics that Google uses as ranking signals. In March 2024, Interaction to Next Paint (INP) permanently replaced First Input Delay (FID). As of 2026, the three Core Web Vitals are:
| Metric | What It Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading speed — time until largest visible element renders | ≤ 2.5s | 2.5–4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Responsiveness — latency from user interaction to visual response | ≤ 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability — unexpected layout shift during page life | ≤ 0.1 | 0.1–0.25 | > 0.25 |
Why LCP matters most: LCP is the strongest predictor of user-perceived load time. The most common LCP elements are hero images, large text blocks above the fold, and background images. Preloading the LCP element is the single highest-ROI performance optimisation on most websites.
INP replaces FID: FID only measured the first interaction; INP measures the worst-case interaction latency across the full page session, making it a much stricter measure of JavaScript runtime performance. Long tasks (>50ms on the main thread) are the primary cause of poor INP scores.
2. Measuring Performance: Tools in 2026
- Lighthouse (Chrome DevTools): Lab measurement — synthetic test from a throttled connection. Best for development feedback. Run with
lighthouse https://yoursite.com --only-categories=performance. - Google PageSpeed Insights: Combines Lighthouse lab data with real-world CrUX (Chrome User Experience Report) field data for your URL. The "field data" section is what Google actually uses for ranking.
- Chrome User Experience Report (CrUX): Anonymous real-user data collected by Chrome. Available in PageSpeed Insights and via BigQuery for bulk analysis.
- WebPageTest: The most configurable lab tool. Supports real devices, multiple test locations, visual comparison, and waterfall analysis. Best for diagnosing root causes.
- Core Web Vitals report in Google Search Console: Shows the real-world URL count passing/failing each CWV metric across your entire site.
# Install globally
npm install -g lighthouse
# Run a performance-only audit and output JSON
lighthouse https://example.com \
--only-categories=performance \
--output=json \
--output-path=./report.json \
--chrome-flags="--headless"
# Automated CI check: fail if performance score < 90
node -e "
const r = require('./report.json');
const score = r.categories.performance.score * 100;
console.log('Performance score:', score);
if (score < 90) process.exit(1);
"
3. Image Optimization
Images account for 40–60% of total page weight on most websites. They are almost always the biggest performance win and the root cause of poor LCP scores.
3.1 Use Modern Formats
AVIF is the best-supported modern image format in 2026, offering 50% smaller file sizes than JPEG at equivalent quality. WebP offers 30% smaller sizes. Use both with a JPEG fallback via the <picture> element:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" width="1200" height="630"
loading="eager" fetchpriority="high">
</picture>
3.2 LCP Image Preloading
The LCP image should be preloaded in the <head> to avoid discovery delays. This single change often improves LCP by 300–600ms:
<link rel="preload" as="image" href="/hero.avif"
imagesrcset="/hero-480.avif 480w, /hero-800.avif 800w, /hero.avif 1200w"
imagesizes="(max-width: 600px) 480px, (max-width: 900px) 800px, 1200px"
fetchpriority="high">
3.3 Correct Width/Height Attributes
Always specify width and height attributes on <img> tags. The browser uses these to reserve space before the image loads, eliminating CLS caused by images loading into unstaged layouts.
3.4 Lazy Loading Below the Fold
Add loading="lazy" to all images not visible in the initial viewport. Do NOT add it to the LCP image — this would defeat the purpose since lazy loading defers resource loading.
4. JavaScript Optimization
JavaScript is the most common cause of poor INP scores. Long tasks on the main thread block user interaction responses. The goal is to ensure no JavaScript task runs for more than 50ms without yielding to the browser.
4.1 Code Splitting
Bundle splitting divides your JavaScript into smaller chunks loaded on demand. With Vite or Webpack:
// Vite config: automatic code splitting by route
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
// Split vendor libraries into a separate chunk
vendor: ['react', 'react-dom'],
// Split UI library
ui: ['@radix-ui/react-dialog', '@radix-ui/react-tooltip'],
}
}
}
}
};
// Dynamic import for route-based code splitting
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
4.2 Tree Shaking
Tree shaking removes unused code from your bundle. It requires ES Module imports (not CommonJS require()). Import only what you use: import { debounce } from 'lodash-es' instead of import _ from 'lodash' saves ~70KB.
4.3 Yield to the Main Thread (INP Fix)
Break long tasks into smaller pieces by yielding control back to the browser after each chunk:
// Use scheduler.yield() (Chrome 115+) to break up long tasks
async function processLargeDataset(items) {
const CHUNK_SIZE = 50;
const results = [];
for (let i = 0; i < items.length; i++) {
results.push(processItem(items[i]));
// Yield every 50 items — gives browser chance to handle user input
if (i % CHUNK_SIZE === 0 && i > 0) {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
// Fallback: setTimeout(0) is slower but works everywhere
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
return results;
}
5. CSS Optimization
- Eliminate render-blocking CSS: CSS in
<head>blocks rendering. Split into critical (above-the-fold) and non-critical. Inline critical CSS; load non-critical async withmedia="print" onload="this.media='all'". - Remove unused CSS: Tools like PurgeCSS or Vite's built-in CSS minification analyse your HTML and remove CSS rules that are never used. On large sites, this can reduce CSS size by 80%.
- Avoid CSS
@import: CSS@importcreates serial waterfall requests. Use<link>tags instead, or bundle CSS with your build tool. - Use
content-visibility: auto: Tells the browser to skip rendering off-screen content until the user scrolls to it. Can reduce initial render time by 30–50% on long pages.
6. Web Font Strategy
Web fonts are a common cause of both layout shift (CLS) and delayed rendering (LCP). Best practices:
- Use
font-display: swapto show fallback text immediately while the custom font loads. - Preload critical fonts:
<link rel="preload" as="font" href="/fonts/main.woff2" crossorigin>. - Self-host fonts instead of loading from Google Fonts — eliminates a cross-origin DNS lookup and connection establishment (~100–300ms).
- Use the
size-adjustdescriptor to make your fallback font the same size as the target font, eliminating CLS when the custom font loads. - Subset fonts to include only the characters you use — reduces font file size by 70–90% for Latin-only sites.
7. Server & CDN Strategy
Time to First Byte (TTFB) should be under 600ms for a Good rating. TTFB is outside LCP but directly affects it since the browser cannot start loading any asset until TTFB completes.
- Use a CDN for static assets: Cloudflare, Fastly, and AWS CloudFront serve assets from edge locations near the user, reducing TTFB from 200–500ms to 10–50ms for globally distributed users.
- HTTP/3 (QUIC): HTTP/3 eliminates head-of-line blocking at the transport layer. All major CDNs and servers support it in 2026. Enable in Nginx with
listen 443 quic reuseport. - Early Hints (103): Send Link preload headers before the main HTML response is ready, letting the browser start loading critical assets during server processing time.
- Compression: Use Brotli (20–25% smaller than gzip) for text assets. Zstandard (zstd) is supported in Firefox and Chrome and offers better compression ratios for larger files.
8. Caching Strategy
| Asset Type | Cache-Control Header | Rationale |
|---|---|---|
Hashed JS/CSS bundles (e.g. main.a3f8b.js) | public, max-age=31536000, immutable | Content-addressed — URL changes when file changes; safe to cache forever |
Images (logo.avif) | public, max-age=2592000 (30 days) | Long cache; bust by changing filename or adding query string |
| HTML pages | public, max-age=0, must-revalidate | Always revalidate — HTML is the entry point and must be fresh |
| API responses | private, max-age=60, stale-while-revalidate=300 | Cache briefly in browser; serve stale while fetching fresh in background |
Fonts (.woff2) | public, max-age=31536000, immutable | Same as hashed assets; fonts rarely change |
9. Rendering Patterns: SSR, SSG, ISR
The rendering pattern is the architectural decision that most affects TTFB, LCP, and SEO simultaneously:
- SSG (Static Site Generation): HTML generated at build time. TTFB <50ms from CDN edge. Best for content that doesn't change per-user (blogs, marketing pages, documentation). This site uses SSG.
- SSR (Server-Side Rendering): HTML generated per request. Higher TTFB than SSG but always fresh content. Best for personalised, authenticated pages. Edge SSR (Cloudflare Workers, Vercel Edge Functions) reduces TTFB by running SSR at CDN edge nodes globally.
- ISR (Incremental Static Regeneration): Next.js feature. Pages are statically generated and cached, but revalidated in the background after a configurable time interval. Best of both worlds for semi-dynamic content.
- SPA (Single Page Application): Worst for initial load performance — requires JS execution before content renders. LCP scores for client-rendered SPAs are typically 1–2 seconds worse than SSR/SSG equivalents.
10. Performance Checklist
| Category | Action | Impact | Effort |
|---|---|---|---|
| Images | Convert to AVIF/WebP | High | Low |
| Images | Preload LCP image | Very High | Low |
| Images | Add width/height attributes | Medium (CLS) | Low |
| Images | Lazy load below-fold images | Medium | Low |
| JavaScript | Code splitting by route | High | Medium |
| JavaScript | Remove unused dependencies | High | Medium |
| JavaScript | Yield long tasks (>50ms) | High (INP) | Medium |
| CSS | Inline critical CSS | Medium | Medium |
| CSS | Remove unused CSS (PurgeCSS) | Medium | Low |
| Fonts | Self-host + preload | Medium | Low |
| Server | Enable CDN + Brotli | Very High | Low |
| Server | Enable HTTP/3 | Medium | Low |
| Caching | Immutable cache for hashed assets | High (returning visitors) | Low |
11. Frequently Asked Questions
Does Lighthouse score still matter for SEO in 2026?
Lighthouse is a lab tool — Google uses real-user CrUX data (field data) for ranking, not Lighthouse lab scores. However, a high Lighthouse score is a reliable proxy for good real-world performance. A site scoring 90+ in Lighthouse almost always has good CrUX data too. Focus on the field data section of PageSpeed Insights for actual ranking impact.
What is the single fastest win for improving LCP?
Adding fetchpriority="high" and a <link rel="preload"> for the LCP image. This tells the browser to start fetching the LCP image immediately during HTML parsing, before it would normally discover it. For most sites, this reduces LCP by 300–800ms with a single line of HTML.
How do I fix CLS caused by ads or embeds?
Reserve space for ads and embeds before they load by setting fixed min-height on ad containers: min-height: 250px for a standard banner. For dynamic content that changes size, use the CSS aspect-ratio property combined with known dimensions.
12. Glossary
- LCP (Largest Contentful Paint)
- Time from page navigation until the largest visible element renders. Target: ≤ 2.5s.
- INP (Interaction to Next Paint)
- Average interaction latency — time between user input and the next visual update. Target: ≤ 200ms.
- CLS (Cumulative Layout Shift)
- Score measuring unexpected visual movement during page load. Target: ≤ 0.1.
- TTFB (Time to First Byte)
- Time from navigation until first byte of the HTML response is received. Target: ≤ 600ms.
- CrUX (Chrome User Experience Report)
- Anonymous real-user performance data collected from Chrome browsers across the web.
- Code Splitting
- Breaking a JavaScript bundle into smaller chunks loaded on demand rather than all at page load.
- Tree Shaking
- Removing unused code from JavaScript bundles during build, reducing bundle size.
13. References & Further Reading
- web.dev — Core Web Vitals (Google)
- web.dev — Largest Contentful Paint
- web.dev — Interaction to Next Paint
- Google PageSpeed Insights
- WebPageTest — Advanced Performance Analysis
- Lighthouse Documentation
Start with PageSpeed Insights on your live URL today. Identify your LCP element, preload it, and convert your hero image to AVIF. Those two changes alone commonly push LCP from 4s to under 2.5s.