Mastering Core Web Vitals
Google's Core Web Vitals measure user experience. They directly impact search ranking and user retention. Optimizing them is non-negotiable.
The Three Vitals
Largest Contentful Paint (LCP): Time until the main content is visible and interactive. Target: <2.5 seconds.
Interaction to Next Paint (INP): Time from user input (click, tap) to visual feedback. Target: <200ms.
Cumulative Layout Shift (CLS): How much the page shifts during load. Target: <0.1.
Optimizing LCP
LCP measures when the largest visible element renders. Common culprits and fixes:
1. Server-side rendering: Render initial HTML on server, not browser.
// Bad: Client-side render
export default function Page() {
const [posts, setPosts] = useState()
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])
if (!posts) return <Loading />
return <div>{posts.map(...)}</div>
}
// Good: Server-side fetch
async function PostsPage() {
const posts = await fetch('https://api.example.com/posts')
return <div>{posts.map(...)}</div>
}
2. Image optimization: Large images delay LCP significantly.
- Use WebP with JPEG fallback.
- Serve responsive images:
<img srcset="small.jpg 480w, large.jpg 1200w" sizes="..." /> - Lazy-load below-the-fold images:
<img loading="lazy" /> - Use
fetch-priority="high"on critical images.
3. Font loading: Web fonts block rendering. Use font-display: swap:
@font-face {
font-family: 'Custom';
src: url('custom.woff2') format('woff2');
font-display: swap; /* Use fallback while loading */
}
Or preload critical fonts:
<link rel="preload" as="font" href="custom.woff2" crossorigin />
4. CSS and JavaScript: Minimize render-blocking resources.
- Inline critical CSS (above-the-fold).
- Defer non-critical JavaScript:
<script defer>. - Use
asyncfor analytics and non-blocking third-party scripts.
Optimizing INP
INP measures responsiveness. It's slower than First Input Delay (FID) because it measures all interactions.
1. Avoid long tasks: Anything over 50ms blocks interaction.
// Bad: Blocks for >50ms
function expensiveCalculation() {
for (let i = 0; i < 100000000; i++) {
// Heavy computation
}
}
// Good: Break into chunks
async function expensiveCalculation() {
for (let i = 0; i < 10000000; i++) {
if (i % 1000000 === 0) await new Promise(r => setTimeout(r, 0))
}
}
2. Use debouncing and throttling:
function debounce(fn, delay) {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
window.addEventListener('resize', debounce(handleResize, 200))
3. Optimize input handlers: Minimize work inside click/submit handlers.
// Bad: Heavy computation on input
input.addEventListener('change', () => {
const result = expensiveCalculation(input.value)
updateUI(result)
})
// Good: Debounce expensive work
const debouncedHandle = debounce(() => {
const result = expensiveCalculation(input.value)
updateUI(result)
}, 300)
input.addEventListener('change', debouncedHandle)
Optimizing CLS
CLS measures visual stability. Shifts frustrate users and cause misdirected clicks.
1. Reserve space for dynamic content:
// Bad: Ad loads and shifts content
<div>
{/* Ad loads here, shifts everything down */}
<div id="ad-container"></div>
<article>Your content</article>
</div>
// Good: Reserve space
<div style={{ height: '280px' }}>
<div id="ad-container"></div>
</div>
<article>Your content</article>
2. Animations and transitions: Use transform and opacity, not width or height.
/* Bad: Causes layout shift */
.box { width: 100px; transition: width 0.3s; }
.box:hover { width: 200px; }
/* Good: No layout shift */
.box { transform: scale(1); transition: transform 0.3s; }
.box:hover { transform: scale(2); }
3. Avoid inserting content above the fold dynamically.
Measurement Tools
- Google PageSpeed Insights: Lab and field data, recommendations.
- WebPageTest: Waterfall charts, video, detailed diagnostics.
- Chrome DevTools Lighthouse: Local testing during development.
- Web Vitals JavaScript Library: Monitor real-user metrics.
import { getLCP, getFID, getCLS } from 'web-vitals'
getCLS(console.log) // Logs CLS as it changes
getFID(console.log)
getLCP(console.log)
Real-World Impact
A SaaS company optimized LCP from 4.5s to 2.2s, INP from 300ms to 180ms, and CLS from 0.25 to 0.08. Results: 25% more conversions, 18% lower bounce rate, and ranking boost for 40+ keywords.
Frequently asked questions
What's a good LCP score?
Under 2.5 seconds is good. 2.5-4 seconds is fair. Over 4 seconds is poor. Measure on real devices and networks, not just desktop.
How do I fix cumulative layout shift?
Reserve space for dynamic content, avoid DOM insertions above the fold, and use transform/opacity instead of width/height animations.
Does server-side rendering always improve LCP?
Usually, but it depends. Server rendering helps if the server fetches data fast. If server is slow, client-side rendering can be faster.