Understanding Core Web Vitals
Core Web Vitals are three key metrics introduced by Google that measure real-world user experience on your website. These metrics directly impact your SEO ranking and user satisfaction. Understanding and optimizing these metrics is essential for modern web development.
The Three Core Web Vitals
- Largest Contentful Paint (LCP): Measures loading performance. It marks the point when the largest content element becomes visible. Aim for LCP under 2.5 seconds. This ensures your users can see the main content quickly.
- First Input Delay (FID) / Interaction to Next Paint (INP): Measures interactivity. It quantifies the delay between user input and browser response. Target under 100ms. Fast interactivity makes your site feel responsive.
- Cumulative Layout Shift (CLS): Measures visual stability. It tracks unexpected layout shifts during loading. Keep CLS below 0.1. A stable layout provides a better user experience.
Image Optimization - The Biggest Impact
Images typically account for 50-80% of page load time. Proper optimization is crucial for performance.
Format Selection
Choose the right format for the right use case:
- WebP: Modern format, 25-35% smaller than JPEG/PNG. Use with fallbacks for older browsers.
- AVIF: Newest format, even better compression than WebP. Growing browser support.
- JPEG: Best for photographs. Use quality 75-85 for good balance.
- PNG: Use for images requiring transparency or lossless compression.
- SVG: Perfect for logos and icons. Scales infinitely, can be animated.
<!-- Serve multiple formats with fallback -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description">
</picture>
Responsive Images
Serve different image sizes for different screen sizes:
<!-- srcset with pixel density -->
<img
src="image.jpg"
srcset="
image-small.jpg 1x,
image-small-2x.jpg 2x"
alt="Description"
>
<!-- srcset with viewport widths -->
<img
src="image.jpg"
srcset="
image-300.jpg 300w,
image-600.jpg 600w,
image-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
alt="Description"
>
Image Compression
# Using ImageMagick
convert input.jpg -quality 80 output.jpg
# Using cwebp for WebP conversion
cwebp -q 80 input.jpg -o output.webp
# Tools: TinyPNG, ImageOptim, sharp (Node.js)
Lazy Loading Images
<!-- Native lazy loading (supported in modern browsers) -->
<img
src="image.jpg"
loading="lazy"
alt="Description"
>
<!-- With Intersection Observer for more control -->
<img
data-src="image.jpg"
class="lazy-image"
alt="Description"
>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
document.querySelectorAll('.lazy-image').forEach(img => {
observer.observe(img)
})
</script>
JavaScript Optimization
Code Splitting
Modern bundlers enable splitting code into smaller chunks loaded on demand:
// Dynamic imports for code splitting
const HeavyComponent = React.lazy(() => import('./HeavyComponent'))
// Use with Suspense for loading states
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
// Route-based splitting with Next.js
import dynamic from 'next/dynamic'
const Dashboard = dynamic(() => import('./dashboard'), {
loading: () => <LoadingSpinner />,
ssr: false, // optional: disable server-side rendering for this component
})
Tree Shaking
Remove unused code from your bundles by using ES6 modules:
// Good - tree shaking works
import { sortBy, groupBy } from 'lodash-es'
// Bad - whole library included
import _ from 'lodash'
// Build tools remove unused exports
// Only sortBy and groupBy are included in final bundle
Minification and Compression
# gzip compression (90%+ browser support)
# Configured in most web servers
# Brotli compression (better compression than gzip)
# Growing browser support
# Enable in your web server:
# Nginx: gzip on; gzip_types text/css application/javascript;
# Apache: mod_deflate enabled
CSS Optimization
Critical CSS
Inline CSS needed for above-the-fold content to reduce render-blocking:
<!-- Inline critical CSS in head -->
<style>
/* Critical CSS for hero section */
.hero { ... }
.nav { ... }
</style>
<!-- Load rest asynchronously -->
<link rel="preload" href="styles.css" as="style">
<link rel="stylesheet" href="styles.css">
Font Optimization
/* Use font-display to optimize font loading */
@font-face {
font-family: 'CustomFont'
src: url('font.woff2') format('woff2')
font-display: swap; /* Show fallback immediately, swap when custom loads */
}
/* Preload critical fonts */
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
Caching Strategies
HTTP Caching Headers
# Static assets (images, CSS, JS with hashes)
Cache-Control: public, immutable, max-age=31536000
# HTML and dynamic content
Cache-Control: public, max-age=0, must-revalidate
# API responses
Cache-Control: private, max-age=3600
# Don't cache
Cache-Control: no-cache, no-store, must-revalidate
Service Worker Caching
// Cache-first strategy for static assets
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
return caches.open('images-v1').then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
})
)
}
})
// Network-first strategy for API calls
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request).then((response) => {
return caches.open('api-v1').then((cache) => {
cache.put(event.request, response.clone())
return response
})
}).catch(() => {
return caches.match(event.request)
})
)
}
})
Database and API Optimization
Pagination
Always paginate large datasets to avoid slow API responses:
// Good - paginated response
GET /api/posts?page=1&limit=20
{
data: [...], // 20 items
total: 500,
page: 1,
hasMore: true
}
// Bad - fetching everything
GET /api/posts // Returns all 500 items!
Query Optimization
// Bad - N+1 query problem
const users = await db.user.findMany()
for (const user of users) {
user.posts = await db.post.findMany({ where: { userId: user.id } })
// Queries: 1 + N (one for each user!)
}
// Good - use include/select for eager loading
const users = await db.user.findMany({
include: { posts: true }
})
// Queries: 1-2 database queries total
Response Compression
// Only return needed fields
const users = await db.user.findMany({
select: { id: true, name: true, email: true }
// Reduces response size by excluding large fields
})
// Paginate API responses
const posts = await db.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize
})
Monitoring and Measuring Performance
Web Vitals Measurement
// Use web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
getCLS(console.log) // Cumulative Layout Shift
getFID(console.log) // First Input Delay (deprecated, use INP)
getFCP(console.log) // First Contentful Paint
getLCP(console.log) // Largest Contentful Paint
getTTFB(console.log) // Time to First Byte
Performance Observer API
// Monitor performance metrics
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration}ms`)
}
})
observer.observe({ entryTypes: ['navigation', 'resource', 'paint', 'largest-contentful-paint'] })
Tools for Measurement
- Google PageSpeed Insights: Free analysis with recommendations. Shows real user data.
- Lighthouse: Built into Chrome DevTools. Audits performance, accessibility, and SEO.
- WebPageTest: Detailed performance waterfall charts. Free and detailed analysis.
- Real User Monitoring (RUM): Collect actual user performance data using web-vitals library.
- Sentry/DataDog: Performance monitoring and error tracking.
Performance Checklist
- ☐ Optimize images (use WebP, responsive, lazy load)
- ☐ Minimize JavaScript (code splitting, tree shaking)
- ☐ Optimize fonts (preload, font-display: swap)
- ☐ Inline critical CSS
- ☐ Enable gzip/brotli compression
- ☐ Configure proper cache headers
- ☐ Set up Service Worker for offline support
- ☐ Paginate API responses
- ☐ Optimize database queries (avoid N+1)
- ☐ Monitor Core Web Vitals regularly
- ☐ Test on slow networks (Chrome DevTools throttling)
- ☐ Use a CDN for static assets
Real-World Performance Improvements
Case Study: E-commerce site with 8-second load time:
- Image optimization (WebP, responsive): -3s
- Code splitting: -1.5s
- Service Worker caching: -2s (return visits)
- Database query optimization: -1s
- Result: 2-second load time (75% improvement!)
Conclusion
Web performance optimization is an ongoing process that compounds over time. By understanding Core Web Vitals, implementing optimization techniques systematically, and measuring regularly, you can dramatically improve user experience and SEO rankings. Remember: every 100ms of improvement can lead to increased conversions. Start measuring today, identify bottlenecks with Lighthouse or PageSpeed Insights, and implement improvements incrementally. The best optimization you can do is the one you actually measure and validate.