Web Performance: Core Web Vitals Optimization
Understanding Core Web Vitals
LCP (Largest Contentful Paint) - Loading performance
Good: < 2.5s, Needs Improvement: 2.5-4s, Poor: > 4s
CLS (Cumulative Layout Shift) - Visual stability
Good: < 0.1, Needs Improvement: 0.1-0.25, Poor: > 0.25
INP (Interaction to Next Paint) - Responsiveness (replaced FID)
Good: < 200ms, Needs Improvement: 200-500ms, Poor: > 500ms
LCP Optimization
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- Use modern image formats -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="Hero" width="1200" height="630" loading="eager">
</picture>
// Next.js: Priority for LCP image
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // Preloads with fetchpriority="high"
sizes="100vw"
/>
Image Optimization
// Responsive images with srcset
function ResponsiveImage({ src, alt }: { src: string; alt: string }) {
return (
<img
src={`${src}?w=800`}
srcSet={`${src}?w=400 400w, ${src}?w=800 800w, ${src}?w=1200 1200w`}
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
alt={alt}
loading="lazy"
decoding="async"
/>
);
}
// Convert to WebP/AVIF with sharp
import sharp from 'sharp';
async function optimizeImage(inputPath: string, outputDir: string): Promise<void> {
const base = path.basename(inputPath, path.extname(inputPath));
await sharp(inputPath)
.resize(1200, null, { withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(`${outputDir}/${base}.webp`);
await sharp(inputPath)
.avif({ quality: 65 })
.toFile(`${outputDir}/${base}.avif`);
}
CLS Prevention
/* Reserve space for images before they load */
img {
aspect-ratio: 16/9;
width: 100%;
height: auto;
}
/* Reserve space for dynamic content */
.ad-container {
min-height: 250px; /* Expected ad height */
}
/* Avoid layout shifts from fonts */
@font-face {
font-family: 'Inter';
font-display: swap; /* Use fallback font while loading */
}
JavaScript Performance
// Defer non-critical JavaScript
// Bad: Blocks rendering
<script src="/analytics.js"></script>
// Good: Load after page is interactive
<script src="/analytics.js" defer></script>
// or async for independent scripts
<script src="/analytics.js" async></script>
// Measure INP
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'event') {
const inp = entry.duration;
if (inp > 200) {
console.warn(`Slow interaction: ${entry.name} took ${inp}ms`);
// Report to analytics
}
}
}
});
observer.observe({ type: 'event', buffered: true });
Performance Monitoring
// Report Web Vitals to analytics
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics({ name, value, id }: Metric) {
navigator.sendBeacon('/analytics', JSON.stringify({
metric: name,
value: Math.round(name === 'CLS' ? value * 1000 : value),
id,
url: window.location.href,
userAgent: navigator.userAgent,
}));
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
Resource Hints
<!-- Preconnect to critical third parties -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.myapp.com">
<!-- DNS prefetch for non-critical -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- Prefetch next page resources -->
<link rel="prefetch" href="/next-page.js">
Measure first with Lighthouse or WebPageTest, then optimize the biggest wins.