Core Web Vitals are the foundation of user experience on the web. Master them to improve both UX and SEO.
What are Core Web Vitals?
Google's Core Web Vitals measure real-world user experience:
- 1LCP (Largest Contentful Paint): Loading performance
- 2FID (First Input Delay): Interactivity
- 3CLS (Cumulative Layout Shift): Visual stability
Largest Contentful Paint (LCP)
What it Measures
Time from navigation to when the largest content element renders.
Target: < 2.5 seconds
Measure LCP
// Measure LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
console.log('LCP Element:', lastEntry.element);
}).observe({ entryTypes: ['largest-contentful-paint'] });Optimization Strategies
1. Optimize Images
<!-- Use modern formats -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" loading="eager" decoding="async">
</picture>
<!-- Add dimensions to prevent layout shift -->
<img
src="image.jpg"
width="800"
height="600"
loading="lazy"
>2. Preload Critical Resources
<!-- Preload LCP image -->
<link rel="preload" as="image" href="hero.jpg">
<!-- Preload critical CSS -->
<link rel="preload" as="style" href="critical.css">
<!-- Preconnect to origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">3. Eliminate Render-Blocking Resources
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
<!-- Defer JavaScript -->
<script defer src="app.js"></script>4. Server-Side Rendering
Generate HTML on the server for faster initial render.
// Next.js example
export async function getServerSideProps() {
const data = await fetchData();
return {
props: { data }, // Server-rendered HTML
};
}First Input Delay (FID)
What it Measures
Time from user's first interaction to browser response.
Target: < 100 milliseconds
Measure FID
// Measure FID
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
}
}).observe({ entryTypes: ['first-input'] });Optimization Strategies
1. Reduce JavaScript Execution
// Code split by route
const Dashboard = lazy(() => import('./routes/Dashboard'));
// Lazy load components
const HeavyChart = lazy(() => import('./HeavyChart'));
// Use Web Workers for heavy computation
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });2. Defer Non-Critical JavaScript
<!-- Defer non-critical JS -->
<script defer src="analytics.js"></script>
<script defer src="chat-widget.js"></script>
<!-- Load critical JS inline -->
<script>
// Critical only
initNavigation();
</script>3. Use React Concurrent Features
// Use Transitions for non-urgent updates
import { startTransition } from 'react';
startTransition(() => {
setSearchResults(filterLargeList(query));
});
// Use Suspense for lazy loading
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>Cumulative Layout Shift (CLS)
What it Measures
Unexpected layout shifts during page load.
Target: < 0.1
Measure CLS
// Measure CLS
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
console.log('CLS:', clsValue);
}
}
}).observe({ entryTypes: ['layout-shift'] });Optimization Strategies
1. Reserve Space for Dynamic Content
/* Reserve space for images */
.image-container {
aspect-ratio: 16 / 9;
min-height: 300px;
background: #f0f0f0;
}
/* Reserve space for ads */
.ad-slot {
min-height: 250px;
}2. Include Image Dimensions
<!-- Always include width and height -->
<img
src="image.jpg"
width="800"
height="600"
style="aspect-ratio: 800/600;"
>3. Avoid Inserting Content Above Existing
// Bad: Pushes content down
function pushNotification(message) {
const notification = document.createElement('div');
document.body.prepend(notification);
}
// Good: Fixed position overlay
function showNotification(message) {
const notification = document.createElement('div');
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
document.body.appendChild(notification);
}4. Use font-display: swap
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap; /* Prevents FOIT */
}Additional Web Vitals
Time to First Byte (TTFB)
Target: < 800 milliseconds
// Measure TTFB
const navigation = performance.getEntriesByType('navigation')[0];
const ttfb = navigation.responseStart - navigation.requestStart;
console.log('TTFB:', ttfb);Optimization:
- Use CDN
- Enable compression
- Optimize server code
- Use HTTP/2 or HTTP/3
First Contentful Paint (FCP)
Target: < 1.8 seconds
// Measure FCP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime);
}
}
}).observe({ entryTypes: ['paint']});Optimization:
- Reduce render-blocking resources
- Minify CSS and JavaScript
- Critical CSS inlining
Monitoring and Reporting
Real User Monitoring (RUM)
// Send Web Vitals to analytics
import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS((metric) => sendToAnalytics(metric));
getFID((metric) => sendToAnalytics(metric));
getLCP((metric) => sendToAnalytics(metric));
function sendToAnalytics(metric) {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
}),
});
}Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://your-site.com
uploadArtifacts: true
temporaryPublicStorage: trueCommon Issues and Solutions
Large LCP
Issue: Hero image takes too long to load
Solutions:
<!-- 1. Preload hero image -->
<link rel="preload" as="image" href="hero.webp">
<!-- 2. Use AVIF/WebP -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero">
</picture>
<!-- 3. Add width/height -->
<img src="hero.jpg" width="1200" height="630" alt="Hero">Poor FID
Issue: Too much JavaScript on main thread
Solutions:
// 1. Code split
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 2. Defer non-critical JS
<script defer src="analytics.js"></script>
// 3. Use Web Workers
const worker = new Worker('compute.worker.js');High CLS
Issue: Images without dimensions
Solutions:
<!-- 1. Always add dimensions -->
<img src="image.jpg" width="800" height="600">
<!-- 2. Use aspect-ratio -->
<img src="image.jpg" style="aspect-ratio: 4/3;">
<!-- 3. Reserve space -->
<div style="aspect-ratio: 16/9; background: #f0f0f0;">
<img src="image.jpg" style="width: 100%; height: 100%; object-fit: cover;">
</div>Testing Tools
Lighthouse
# Run Lighthouse
lighthouse https://your-site.com --view
# Run in CI
npm run lighthousePageSpeed Insights
# Test with PSI
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://your-site.com"Chrome DevTools
- 1Open DevTools
- 2Go to Performance tab
- 3Record page load
- 4Analyze Web Vitals
Best Practices
DO
- Measure real user performance
- Optimize for Core Web Vitals
- Use modern image formats
- Lazy load non-critical resources
- Monitor continuously
- Test on real devices
DON'T
- Optimize without measuring
- Ignore CLS
- Block rendering with large CSS/JS
- Serve unoptimized images
- Forget about mobile performance
- Assume lab scores equal real-world
Conclusion
Core Web Vitals are essential for both user experience and SEO. Measure first, then optimize. Focus on LCP by optimizing resources, improve FID by reducing JavaScript, and minimize CLS by reserving space for content. Monitor continuously and iterate based on real user data.