Core Web Vitals: How to Optimize Your Website Performance
Complete guide to Google Core Web Vitals. LCP, FID, CLS explained with practical examples and optimization techniques.
Understanding Core Web Vitals
Core Web Vitals are Google's standardized metrics for measuring user experience on websites. Since 2021, they've been a ranking factor in search results, making them essential for both SEO and user satisfaction.
This guide explains what each metric measures, why it matters, and provides practical techniques to optimize your website's performance.
The Three Core Web Vitals
Largest Contentful Paint (LCP)
LCP measures how long it takes for the largest visible content element to render. This is typically a hero image, video thumbnail, or large text block.
- Good: ≤2.5 seconds
- Needs Improvement: 2.5-4 seconds
- Poor: >4 seconds
LCP reflects perceived load speed—it's what users "feel" as the page becoming ready.
Interaction to Next Paint (INP)
INP replaced First Input Delay (FID) in March 2024. It measures the responsiveness of a page to user interactions throughout the entire session, not just the first click.
- Good: ≤200 milliseconds
- Needs Improvement: 200-500 milliseconds
- Poor: >500 milliseconds
INP captures how snappy your page feels when users click buttons, tap links, or type in inputs.
Cumulative Layout Shift (CLS)
CLS measures visual stability—how much elements unexpectedly move around as the page loads.
- Good: ≤0.1
- Needs Improvement: 0.1-0.25
- Poor: >0.25
CLS quantifies the frustrating experience of clicking a button that suddenly moves, or text jumping as ads load.
Measuring Core Web Vitals
Lab Tools
- Chrome DevTools Performance panel: Detailed timeline analysis
- Lighthouse: Automated audits with scores and suggestions
- WebPageTest: Multi-location testing with waterfalls
- PageSpeed Insights: Combined lab and field data
Field Data (Real Users)
- Chrome User Experience Report (CrUX): Real-world data from Chrome users
- Google Search Console: Core Web Vitals report for your site
- web-vitals JavaScript library: Collect your own field data
Implementation Example
// Using web-vitals library
import {onCLS, onINP, onLCP} from 'web-vitals';
function sendToAnalytics({name, delta, id}) {
// Send metric to your analytics endpoint
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({name, delta, id}),
headers: {'Content-Type': 'application/json'}
});
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
Optimizing Largest Contentful Paint
Identify the LCP Element
First, find what's being measured. In Chrome DevTools:
- Open Performance panel
- Record a page load
- Look for "LCP" marker in the timeline
- Check what element triggered it
Optimize Images
If the LCP element is an image:
<!-- Preload hero image -->
<link
rel="preload"
as="image"
href="/hero.webp"
type="image/webp"
fetchpriority="high"
>
<!-- Use modern formats -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="Hero" fetchpriority="high">
</picture>
For detailed image optimization techniques, check our guide on optimizing images for the web.
Eliminate Render-Blocking Resources
<!-- Inline critical CSS -->
<style>
/* Only styles needed for above-the-fold content */
.hero { ... }
.nav { ... }
</style>
<!-- Defer non-critical CSS -->
<link
rel="preload"
href="/styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
>
<!-- Defer JavaScript -->
<script src="/app.js" defer></script>
Server Response Time
Reduce Time to First Byte (TTFB):
- Use CDN for static assets
- Enable server-side caching
- Optimize database queries
- Consider edge rendering (Cloudflare Workers, Vercel Edge)
# nginx caching example
location ~* \.(jpg|jpeg|png|gif|webp|avif|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Font Loading
Fonts can delay LCP if text is the largest element:
<!-- Preload critical fonts -->
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- Use font-display: swap -->
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
</style>
Optimizing Interaction to Next Paint
Identify Slow Interactions
Use Chrome DevTools Performance panel:
- Record a session with interactions
- Look for long tasks (red corners)
- Find interactions with high latency
Reduce JavaScript Execution Time
// Bad: Long synchronous task
function processData(largeArray) {
largeArray.forEach(item => heavyProcessing(item));
}
// Good: Break into smaller chunks
async function processDataChunked(largeArray) {
const chunkSize = 100;
for (let i = 0; i < largeArray.length; i += chunkSize) {
const chunk = largeArray.slice(i, i + chunkSize);
chunk.forEach(item => heavyProcessing(item));
// Yield to main thread
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Use Web Workers
Move heavy computation off the main thread:
// main.js
const worker = new Worker('/worker.js');
worker.postMessage({data: largeDataset});
worker.onmessage = (e) => {
const result = e.data;
updateUI(result);
};
// worker.js
self.onmessage = (e) => {
const result = heavyProcessing(e.data);
self.postMessage(result);
};
Debounce Input Handlers
// Bad: Runs on every keystroke
input.addEventListener('input', (e) => {
expensiveSearch(e.target.value);
});
// Good: Debounced
let timeout;
input.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
expensiveSearch(e.target.value);
}, 150);
});
Use requestIdleCallback
// Defer non-urgent work
requestIdleCallback(() => {
// Analytics, prefetching, etc.
trackPageView();
prefetchNextPage();
}, { timeout: 2000 });
Optimizing Cumulative Layout Shift
Always Include Dimensions
<!-- Always specify width and height -->
<img
src="photo.jpg"
width="800"
height="600"
alt="Description"
>
<!-- For responsive images, use aspect-ratio -->
<style>
.responsive-image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
</style>
Reserve Space for Ads
<style>
.ad-container {
min-height: 250px; /* Reserve space */
background: #f0f0f0;
}
</style>
<div class="ad-container">
<!-- Ad loads here -->
</div>
Avoid Inserting Content Above Existing Content
// Bad: Prepending shifts everything down
container.insertBefore(newElement, container.firstChild);
// Better: Add below existing content
container.appendChild(newElement);
// Or: Use CSS to avoid shift
.notification {
position: fixed;
top: 0;
/* Doesn't affect layout */
}
Font Loading Without Shift
<style>
/* Use size-adjust for fallback font */
@font-face {
font-family: 'Custom Font';
src: url('/custom.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Match fallback metrics to custom font */
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 90%;
}
</style>
Animate with transform and opacity
/* Bad: Animating width/height causes layout shift */
.expand {
animation: expandBad 0.3s;
}
@keyframes expandBad {
from { height: 0; }
to { height: 100px; }
}
/* Good: Use transform instead */
.expand {
animation: expandGood 0.3s;
}
@keyframes expandGood {
from { transform: scaleY(0); }
to { transform: scaleY(1); }
}
Framework-Specific Optimizations
React
// Use React.lazy for code splitting
const HeavyComponent = React.lazy(() =>
import('./HeavyComponent')
);
// Placeholder while loading
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
// Use useDeferredValue for non-urgent updates
const deferredSearch = useDeferredValue(searchTerm);
// Use useTransition for interruptible updates
const [isPending, startTransition] = useTransition();
startTransition(() => {
setSearchResults(results);
});
Next.js
// Use next/image for automatic optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // Preloads for LCP
alt="Hero"
/>
// Use next/font for font optimization
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
Vue
// Async components
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
// Use v-once for static content
<div v-once>{{ staticContent }}</div>
// Use v-memo for expensive renders
<div v-memo="[item.id]">...</div>
Monitoring and Continuous Improvement
Set Up Alerting
Track Core Web Vitals regression in CI/CD:
// Lighthouse CI configuration
module.exports = {
ci: {
assert: {
assertions: {
'largest-contentful-paint': ['warn', {maxNumericValue: 2500}],
'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}],
'interactive': ['warn', {maxNumericValue: 3800}]
}
}
}
};
A/B Testing Performance Changes
Measure impact of optimizations:
- Run experiments with feature flags
- Compare Core Web Vitals between variants
- Use statistical significance testing
Common Issues and Solutions
| Problem | Solution |
|---|---|
| Slow LCP from images | Preload hero image, use modern formats |
| LCP blocked by fonts | Preload fonts, use font-display: swap |
| High INP from third-party scripts | Load asynchronously, use facade pattern |
| INP from heavy event handlers | Debounce, use Web Workers |
| CLS from images | Always include width/height |
| CLS from ads | Reserve space with min-height |
| CLS from fonts | Use size-adjust or local fallbacks |
Tools Quick Reference
For debugging and testing your web performance, use these complementary tools:
- JSON Formatter for analyzing Lighthouse JSON reports
- Regex Tester for building patterns to analyze performance logs
- Diff Checker for comparing before/after metrics
Conclusion
Core Web Vitals are more than SEO checkboxes—they represent real user experience. By optimizing LCP, INP, and CLS, you create faster, more responsive, and more stable websites that users enjoy using.
Key takeaways:
- LCP: Preload critical resources, optimize images, reduce server response time
- INP: Break up long tasks, use Web Workers, debounce handlers
- CLS: Reserve space for dynamic content, always specify image dimensions
For more developer resources, explore our free online tools. For detailed documentation, see web.dev Core Web Vitals and Chrome DevTools Performance docs.