Web Performance Deep Dive
A practical guide to optimizing Core Web Vitals for better user experience and SEO.
Understanding Core Web Vitals is essential for any frontend engineer who cares about user experience. Google measures three key metrics that directly impact your SEO rankings and user retention.
The Three Pillars of Core Web Vitals
Core Web Vitals measure real-world user experience across three dimensions:
- LCP (Largest Contentful Paint) — How fast does the main content load?
- INP (Interaction to Next Paint) — How responsive is the page to user input?
- CLS (Cumulative Layout Shift) — Does the page layout stay stable?
A site that scores poorly on these metrics doesn’t just feel slow — it loses users and search rankings.
Optimizing Largest Contentful Paint
LCP measures when the largest content element becomes visible. This is typically your hero image or heading text.
The Critical Rendering Path
Minimize render-blocking resources. Audit your <head> for synchronous scripts and stylesheets that delay first paint.
<!-- Bad: blocks rendering -->
<script src="/analytics.js"></script>
<link rel="stylesheet" href="/styles.css">
<!-- Good: async or defer -->
<script src="/analytics.js" defer></script>
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
Image Optimization
Image optimization is the single highest-impact LCP improvement for most sites:
<!-- Serve modern formats -->
<img src="hero.avif" alt="Hero image">
<!-- Lazy load offscreen, eager load above fold -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<img src="thumbnail.jpg" loading="lazy" alt="Thumbnail">
Font Loading
Render-blocking fonts cause a “flash of invisible text.” Use font-display: swap and preload critical fonts:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="/fonts/outfit.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'Outfit';
src: url('/fonts/outfit.woff2') format('woff2');
font-display: swap;
}
</style>
Interaction to Next Paint
INP replaced FID as the responsiveness metric. It measures the longest interaction delay throughout the page lifecycle.
Breaking Up Long Tasks
Long tasks — JavaScript that blocks the main thread for over 50ms — are your enemy:
// Bad: blocks main thread for 500ms
function processItems(items) {
items.forEach(item => heavyComputation(item));
}
// Good: yield to browser between chunks
async function processItems(items) {
for (const item of items) {
heavyComputation(item);
// Yield every 50 items
if (items.indexOf(item) % 50 === 0) {
await scheduler.yield();
}
}
}
Web Workers for Background Computation
Move heavy computation off the main thread:
// worker.js
self.addEventListener('message', ({ data }) => {
const result = heavyComputation(data);
self.postMessage(result);
});
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ complexData });
worker.addEventListener('message', ({ data }) => {
updateUI(data);
});
Cumulative Layout Shift
CLS penalizes unexpected layout movement. Users hate it when content jumps while they’re trying to read or click.
Always Set Dimensions on Images
<!-- Bad: causes layout shift -->
<img src="photo.jpg" alt="Photo">
<!-- Good: reserve space -->
<img src="photo.jpg" alt="Photo" width="800" height="600" style="aspect-ratio: 4/3;">
Skeleton Screens for Dynamic Content
Reserve space before content loads:
.skeleton {
background: linear-gradient(90deg, #262626 25%, #1a1a1a 50%, #262626 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Never Insert Content Above Existing Content
The most common CLS culprit is injecting banners or ads above the fold after user interaction. If you must show a cookie banner, reserve that space from the start.
Measuring in Production
Lighthouse is great for lab testing, but real user metrics (RUM) tell the true story:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((metric) => sendToAnalytics('LCP', metric.value));
onINP((metric) => sendToAnalytics('INP', metric.value));
onCLS((metric) => sendToAnalytics('CLS', metric.value));
Set performance budgets in your CI pipeline. If your LCP exceeds 2.5s or your CLS exceeds 0.1, the build should warn or fail.
Key Takeaways
- LCP: Preload hero assets, optimize images, use modern formats
- INP: Break up long tasks, use web workers, yield to browser
- CLS: Reserve space for dynamic content, set explicit image dimensions
Performance optimization is never done — it’s an ongoing practice of measuring, fixing, and measuring again.
← START OF ARCHIVE