Optimizing Core Web Vitals: A Technical Guide
Core Web Vitals are Google ranking factors. Poor scores mean lower rankings. This guide shows you how to systematically improve LCP, INP, and CLS.
The Three Core Web Vitals
LCP (Largest Contentful Paint)
What it measures: How quickly the largest visible element loads.
Target value: under 2.5 seconds
Most common problems and solutions:
- Large images: Use
loading="lazy"and modern formats (WebP, AVIF) - Render-blocking resources: Load CSS and JS asynchronously
- Slow server: Use a CDN, optimize server response time
- Client-side rendering: Use prerendering for static content
INP (Interaction to Next Paint)
What it measures: How quickly the page responds to user interactions.
Target value: under 200 milliseconds
Most common problems and solutions:
- Long JavaScript tasks: Code splitting with
React.lazy()andSuspense - Heavy event handlers: Use
requestAnimationFrameorrequestIdleCallback - Excessive re-renders: Use
React.memo(),useMemo(),useCallback()
CLS (Cumulative Layout Shift)
What it measures: How stable the layout remains during loading.
Target value: under 0.1
Most common problems and solutions:
- Images without dimensions: Always specify
widthandheight - Web fonts:
font-display: swapand font preloading - Dynamic content: Use placeholders with fixed dimensions
- Ads and embeds: Define reserved space
Measuring and Monitoring
web-vitals Library
The simplest method for measurement:
import { onCLS, onINP, onLCP } from 'web-vitals'
function sendToAnalytics(metric) {
gtag('event', metric.name, {
value: Math.round(metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
})
}
onCLS(sendToAnalytics)
onINP(sendToAnalytics)
onLCP(sendToAnalytics)
Google Search Console
The Core Web Vitals Report in Search Console shows aggregated data from your real users. Check it regularly.
Chrome DevTools
In the Performance Tab, you can analyze individual metrics in detail. Particularly useful for diagnosing INP issues.
React-Specific Optimizations
Lazy Loading with Code Splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'))
function App() {
return (
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
)
}
Prerendering for SEO
Single-page apps inherently have poor LCP scores since the browser must first load and execute JavaScript. Build-time prerendering solves this problem:
- Vite builds the app
- Playwright visits each route
- The rendered HTML is saved as a static file
- Search engines immediately receive the finished content
Image Optimization
<img
src="/hero.webp"
alt="Hero image"
width={1200}
height={630}
loading="lazy"
decoding="async"
/>
For above-the-fold images: loading="eager" and fetchPriority="high".
Checklist
- [ ] LCP under 2.5s for all important pages
- [ ] INP under 200ms (test with real interactions)
- [ ] CLS under 0.1 (especially check on mobile)
- [ ] Images in WebP/AVIF with explicit dimensions
- [ ] Fonts preloaded with
font-display: swap - [ ] JavaScript bundle under 200 KB (gzipped)
- [ ] web-vitals library installed and connected to analytics
- [ ] Regular review in Search Console
Conclusion
Core Web Vitals are not a one-time project but an ongoing process. Measure regularly, set priorities based on data, and optimize step by step. The biggest gains are almost always achieved with images and JavaScript bundle size.