Skip to main content

Web Performance Optimization: Core Web Vitals and Beyond

Master web performance optimization with Core Web Vitals. Learn to optimize LCP, INP, and CLS with practical strategies, code examples, and real-world techniques.

Table of Contents

Introduction

Web performance optimization has become one of the most critical factors for success in modern web development. With users expecting instant page loads and search engines prioritizing fast, responsive websites, understanding and optimizing Core Web Vitals is no longer optional—it’s essential.

Core Web Vitals are a set of metrics that Google uses to measure real-world user experience on your website. These metrics directly impact your search rankings, user engagement, conversion rates, and overall business success. A slow website doesn’t just frustrate users—it costs you money, reduces your search visibility, and damages your brand reputation.

This comprehensive guide will teach you everything you need to know about Core Web Vitals and web performance optimization. You’ll learn what each metric means, why it matters, and how to optimize it with practical, actionable strategies. We’ll cover everything from image optimization and code splitting to advanced caching techniques and performance monitoring tools.

By the end of this guide, you’ll have a complete understanding of web performance optimization and the tools needed to create blazing-fast websites that rank well in search engines and delight your users.


Understanding Core Web Vitals

Core Web Vitals are a subset of Web Vitals, focusing on three specific aspects of user experience: loading performance, interactivity, and visual stability. Google introduced these metrics as ranking factors in 2021, making them crucial for SEO and user experience.

The Three Core Metrics

The current Core Web Vitals consist of:

  1. Largest Contentful Paint (LCP): Measures loading performance
  2. Interaction to Next Paint (INP): Measures interactivity (replaced First Input Delay in 2024)
  3. Cumulative Layout Shift (CLS): Measures visual stability

Each metric has specific thresholds that determine whether your site provides a “good,” “needs improvement,” or “poor” user experience:

MetricGoodNeeds ImprovementPoor
LCP≤ 2.5s2.5s - 4.0s> 4.0s
INP≤ 200ms200ms - 500ms> 500ms
CLS≤ 0.10.1 - 0.25> 0.25

Why Core Web Vitals Matter

Core Web Vitals impact your website in multiple ways:

  • SEO Rankings: Google uses these metrics as ranking signals in search results
  • User Experience: Fast, responsive sites keep users engaged and reduce bounce rates
  • Conversion Rates: Every 100ms delay can reduce conversions by up to 1%
  • Mobile Performance: Critical for mobile users who often have slower connections
  • Business Metrics: Performance directly correlates with revenue and user satisfaction

⚠️ Important: Core Web Vitals are measured using real user data (Real User Monitoring) from the Chrome User Experience Report (CrUX), not just lab-based tools. This means you need to optimize for actual user experiences, not just synthetic test results.

How Core Web Vitals Are Measured

Core Web Vitals are measured using the Web Vitals API, which provides JavaScript APIs to measure these metrics in real browsers. The metrics are collected from real users visiting your site and aggregated in tools like Google Search Console and PageSpeed Insights.

// Example: Measuring Core Web Vitals with the web-vitals library
import { onLCP, onINP, onCLS } from "web-vitals";
function sendToAnalytics(metric) {
// Send metric to your analytics service
console.log(metric);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Largest Contentful Paint (LCP)

Largest Contentful Paint (LCP) measures how long it takes for the largest content element visible in the viewport to render. This metric is crucial because it represents when users perceive the main content of your page as loaded.

What Counts as LCP?

The LCP element is typically one of these:

  • <img> elements
  • <video> elements with a poster image
  • Elements with a background image loaded via CSS (url())
  • Block-level elements containing text nodes
<!-- Example: Large hero image that's likely the LCP element -->
<img
src="hero-image.jpg"
alt="Hero image"
loading="eager"
fetchpriority="high"
/>

Common LCP Issues

Several factors can negatively impact your LCP score:

  1. Slow server response times: Time to First Byte (TTFB) delays
  2. Render-blocking resources: CSS and JavaScript that block rendering
  3. Large images: Unoptimized images that take too long to load
  4. Client-side rendering: JavaScript-heavy frameworks delaying content
  5. Resource load times: Slow loading of fonts, CSS, or critical resources

Optimizing LCP

1. Optimize Server Response Time

Reduce Time to First Byte (TTFB) by optimizing your server and using CDNs:

// Example: Server-side optimization with caching headers
// Node.js/Express example
app.use((req, res, next) => {
// Cache static assets
if (req.path.match(/\.(jpg|jpeg|png|gif|css|js)$/)) {
res.setHeader("Cache-Control", "public, max-age=31536000");
}
// Enable compression
res.setHeader("Content-Encoding", "gzip");
next();
});

2. Optimize Images

Use modern image formats, proper sizing, and lazy loading:

<!-- ✅ Optimized image with modern format and responsive sizing -->
<picture>
<source srcset="hero-image.avif" type="image/avif" />
<source srcset="hero-image.webp" type="image/webp" />
<img
src="hero-image.jpg"
alt="Hero image"
width="1200"
height="600"
loading="eager"
fetchpriority="high"
decoding="async"
/>
</picture>
/* ✅ Optimize background images */
.hero {
background-image: url("hero-image.avif");
background-size: cover;
background-position: center;
/* Use content-visibility for off-screen content */
content-visibility: auto;
}

3. Eliminate Render-Blocking Resources

Defer non-critical CSS and JavaScript:

<!-- ❌ Render-blocking CSS -->
<link rel="stylesheet" href="styles.css" />
<!-- ✅ Non-blocking 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 non-critical JavaScript -->
<script src="analytics.js" defer></script>

4. Use Resource Hints

Preconnect, prefetch, and preload critical resources:

<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
<!-- Preload critical resources -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-image.jpg" as="image" fetchpriority="high" />
<!-- Prefetch resources for next page -->
<link rel="prefetch" href="next-page.html" />

5. Optimize Critical Rendering Path

Inline critical CSS and optimize above-the-fold content:

<!-- ✅ Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
body {
margin: 0;
font-family: system-ui;
}
.hero {
height: 100vh;
display: flex;
align-items: center;
}
</style>
<!-- Load non-critical CSS asynchronously -->
<link
rel="stylesheet"
href="non-critical.css"
media="print"
onload="this.media='all'"
/>

💡 Pro Tip: Use tools like Critical CSS Generator to extract and inline critical CSS automatically.


Interaction to Next Paint (INP)

Interaction to Next Paint (INP) measures the time from when a user interacts with your page (click, tap, keypress) until the next frame is painted. INP replaced First Input Delay (FID) in March 2024 as a Core Web Vital because it provides a more comprehensive measure of interactivity.

Understanding INP

INP measures the latency of all user interactions throughout the page lifecycle, not just the first one. It captures:

  • Input delay: Time from interaction until event handlers start running
  • Processing time: Time spent executing event handlers
  • Presentation delay: Time until the browser presents the next frame
// ❌ Poor: Long-running event handler blocks interaction
button.addEventListener("click", () => {
// Synchronous operation blocks the main thread
for (let i = 0; i < 1000000; i++) {
// Heavy computation
processData();
}
updateUI();
});
// ✅ Good: Break up work using requestIdleCallback or setTimeout
button.addEventListener("click", () => {
// Use requestIdleCallback for non-critical work
requestIdleCallback(() => {
processData();
});
// Update UI immediately
updateUI();
});

Common INP Issues

  1. Long JavaScript tasks: Event handlers that take too long to execute
  2. Main thread blocking: Heavy computations blocking user interactions
  3. Large JavaScript bundles: Too much JavaScript parsing and execution
  4. Inefficient event handlers: Event handlers that do unnecessary work
  5. Third-party scripts: Analytics, ads, or widgets blocking interactions

Optimizing INP

1. Break Up Long Tasks

Split long-running JavaScript into smaller chunks:

// ❌ Long task blocking interactions
function processLargeDataset(data) {
data.forEach((item) => {
// Heavy processing
processItem(item);
});
}
// ✅ Break into smaller chunks
function processLargeDataset(data) {
let index = 0;
function processChunk() {
const chunkSize = 50;
const end = Math.min(index + chunkSize, data.length);
for (let i = index; i < end; i++) {
processItem(data[i]);
}
index = end;
if (index < data.length) {
// Yield to browser, then continue
setTimeout(processChunk, 0);
}
}
processChunk();
}

2. Use Web Workers for Heavy Computation

Offload heavy work to Web Workers:

// main.js - ✅ Delegate heavy work to Web Worker
const worker = new Worker("worker.js");
button.addEventListener("click", () => {
// Send data to worker
worker.postMessage({ data: largeDataset });
// UI remains responsive
showLoadingState();
});
worker.onmessage = (event) => {
// Receive processed data
updateUI(event.data);
hideLoadingState();
};
// worker.js - Heavy computation in background thread
self.onmessage = (event) => {
const { data } = event.data;
const processed = data.map((item) => {
// Heavy computation that won't block main thread
return processItem(item);
});
self.postMessage(processed);
};

3. Debounce and Throttle Event Handlers

Prevent excessive event handler execution:

// ✅ Debounce: Wait for pause in events
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Apply to search input
const debouncedSearch = debounce((query) => {
performSearch(query);
}, 300);
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
// ✅ Throttle: Limit execution frequency
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Apply to scroll events
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 100);
window.addEventListener("scroll", throttledScroll);

4. Optimize Event Handler Performance

Make event handlers as efficient as possible:

// ❌ Inefficient: Querying DOM multiple times
button.addEventListener("click", () => {
const element1 = document.querySelector(".element1");
const element2 = document.querySelector(".element2");
const element3 = document.querySelector(".element3");
// Multiple DOM queries
});
// ✅ Efficient: Cache DOM references
const element1 = document.querySelector(".element1");
const element2 = document.querySelector(".element2");
const element3 = document.querySelector(".element3");
button.addEventListener("click", () => {
// Use cached references
element1.classList.add("active");
element2.textContent = "Updated";
element3.style.display = "block";
});
// ✅ Use event delegation for multiple elements
document.addEventListener("click", (e) => {
if (e.target.matches(".button")) {
// Handle click efficiently
handleButtonClick(e.target);
}
});

5. Code Split and Lazy Load JavaScript

Reduce initial JavaScript bundle size:

// ✅ Lazy load non-critical functionality
button.addEventListener("click", async () => {
// Dynamically import module only when needed
const { heavyFunction } = await import("./heavy-module.js");
heavyFunction();
});
// ✅ Code splitting with dynamic imports
// Instead of importing everything upfront:
// import { feature1, feature2, feature3 } from './features';
// Load features on demand:
const loadFeature = async (featureName) => {
const module = await import(`./features/${featureName}.js`);
return module.default;
};

Cumulative Layout Shift (CLS)

Cumulative Layout Shift (CLS) measures the visual stability of your page by quantifying how much visible content shifts during the loading process. A low CLS score means your page is visually stable, while a high score indicates jarring layout shifts that frustrate users.

Understanding CLS

CLS measures unexpected layout shifts caused by:

  • Images or videos without dimensions
  • Dynamically injected content
  • Web fonts causing FOIT/FOUT (Flash of Invisible/Unstyled Text)
  • Ads, embeds, or iframes without reserved space
  • Actions that update DOM elements
<!-- ❌ Causes layout shift: Image without dimensions -->
<img src="banner.jpg" alt="Banner" />
<!-- ✅ Prevents layout shift: Image with dimensions -->
<img src="banner.jpg" alt="Banner" width="1200" height="400" loading="lazy" />

Calculating CLS

CLS is calculated using the formula:

CLS = Σ (Impact Fraction × Distance Fraction)
  • Impact Fraction: Proportion of viewport affected by the shift
  • Distance Fraction: Distance the element moved (as a fraction of viewport)

Common CLS Issues

  1. Images without dimensions: Images loading and pushing content down
  2. Web fonts: Fonts loading and causing text reflow
  3. Dynamically injected content: Ads, embeds, or widgets without reserved space
  4. Animations: CSS animations or transitions causing shifts
  5. Third-party content: External scripts adding content without space reservation

Optimizing CLS

1. Always Specify Image Dimensions

Always include width and height attributes:

<!-- ✅ Always include dimensions -->
<img src="product.jpg" alt="Product" width="400" height="300" loading="lazy" />
<!-- ✅ For responsive images, use aspect-ratio CSS -->
<img
src="product.jpg"
alt="Product"
width="400"
height="300"
style="width: 100%; height: auto; aspect-ratio: 4/3;"
/>
/* ✅ Use aspect-ratio for responsive images */
.responsive-image {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* Maintains aspect ratio */
object-fit: cover;
}

2. Reserve Space for Dynamic Content

Reserve space for content that loads dynamically:

<!-- ✅ Reserve space for ad or embed -->
<div class="ad-container" style="min-height: 250px;">
<div id="ad-slot">
<!-- Ad will load here -->
</div>
</div>
<!-- ✅ Use skeleton screens for loading content -->
<div class="content-skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
/* Skeleton screen styles */
.skeleton-line {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 10px;
border-radius: 4px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

3. Optimize Web Fonts

Prevent font-related layout shifts:

<!-- ✅ Preload critical fonts -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
<!-- ✅ Use font-display: swap to prevent FOIT -->
<link rel="stylesheet" href="fonts.css" />
/* ✅ Optimize font loading */
@font-face {
font-family: "CustomFont";
src: url("font.woff2") format("woff2");
font-display: swap; /* Show fallback immediately */
font-weight: 400;
font-style: normal;
}
/* ✅ Use size-adjust to match fallback font metrics */
@font-face {
font-family: "CustomFont";
src: url("font.woff2") format("woff2");
font-display: swap;
size-adjust: 100%; /* Match fallback font size */
}

4. Avoid Inserting Content Above Existing Content

Don’t insert content that pushes existing content down:

// ❌ Bad: Inserts content at top, pushing everything down
function showNotification(message) {
const notification = document.createElement("div");
notification.textContent = message;
document.body.insertBefore(notification, document.body.firstChild);
}
// ✅ Good: Use fixed or absolute positioning
function showNotification(message) {
const notification = document.createElement("div");
notification.textContent = message;
notification.style.position = "fixed";
notification.style.top = "20px";
notification.style.right = "20px";
notification.style.zIndex = "1000";
document.body.appendChild(notification);
}

5. Use CSS Containment

Use CSS containment to isolate layout changes:

/* ✅ Contain layout changes to specific elements */
.widget {
contain: layout style paint;
/* Prevents layout shifts from affecting parent */
}
.card {
contain: layout;
/* Isolates card's layout from rest of page */
}

Additional Performance Metrics

While Core Web Vitals are crucial, other performance metrics provide additional insights into your website’s performance:

Time to First Byte (TTFB)

TTFB measures the time from when a user requests a page until the first byte of the response arrives. It’s a key indicator of server performance.

// Measure TTFB using Performance API
const navigation = performance.getEntriesByType("navigation")[0];
const ttfb = navigation.responseStart - navigation.requestStart;
console.log(`TTFB: ${ttfb}ms`);
// Good: < 200ms
// Needs improvement: 200ms - 500ms
// Poor: > 500ms

First Contentful Paint (FCP)

FCP measures when the first content (text, image, etc.) is painted on the screen. It’s an important early loading metric.

// Measure FCP
const paintEntries = performance.getEntriesByType("paint");
const fcp = paintEntries.find(
(entry) => entry.name === "first-contentful-paint",
);
console.log(`FCP: ${fcp.startTime}ms`);
// Good: < 1.8s
// Needs improvement: 1.8s - 3.0s
// Poor: > 3.0s

Total Blocking Time (TBT)

TBT measures the total amount of time the main thread was blocked, preventing user interactions. It’s a lab metric that correlates with INP.

// Calculate TBT from performance entries
function calculateTBT() {
const entries = performance.getEntriesByType("longtask");
let tbt = 0;
entries.forEach((entry) => {
// Long tasks are > 50ms, blocking time is anything over 50ms
const blockingTime = entry.duration - 50;
if (blockingTime > 0) {
tbt += blockingTime;
}
});
return tbt;
}

Speed Index

Speed Index measures how quickly content is visually displayed during page load. Lower is better.

💡 Pro Tip: Use WebPageTest to get detailed metrics including Speed Index, filmstrip view, and waterfall charts.


Optimization Strategies

Beyond individual metric optimization, here are comprehensive strategies to improve overall web performance:

1. Implement Caching Strategies

Use multiple caching layers for optimal performance:

sw.js
// Service Worker for offline caching
const CACHE_NAME = "v1";
const urlsToCache = ["/", "/styles.css", "/script.js", "/images/logo.png"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)),
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
}),
);
});
<!-- HTTP caching headers -->
<!-- Set appropriate Cache-Control headers server-side -->
<!-- Static assets: long cache -->
<!-- HTML: short cache or no cache -->

2. Minimize and Compress Assets

Reduce file sizes through minification and compression:

// Build process: Minify JavaScript
// Using terser or esbuild
// esbuild example:
// esbuild app.js --bundle --minify --outfile=app.min.js
// Enable Gzip/Brotli compression on server
// Node.js example:
const compression = require("compression");
const express = require("express");
const app = express();
app.use(
compression({
filter: (req, res) => {
if (req.headers["x-no-compression"]) {
return false;
}
return compression.filter(req, res);
},
level: 6, // Compression level
}),
);

3. Use Content Delivery Networks (CDNs)

Serve static assets from CDNs closer to users:

<!-- ✅ Serve assets from CDN -->
<link rel="stylesheet" href="https://cdn.example.com/styles.css" />
<script src="https://cdn.example.com/script.js"></script>
<!-- ✅ Use CDN for images -->
<img src="https://cdn.example.com/images/hero.jpg" alt="Hero" />

4. Implement Resource Prioritization

Tell the browser which resources are most important:

<!-- Critical resources: high priority -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-image.jpg" as="image" fetchpriority="high" />
<!-- Above-the-fold content: high priority -->
<script src="above-fold.js" fetchpriority="high"></script>
<!-- Below-the-fold content: low priority -->
<script src="below-fold.js" fetchpriority="low"></script>

5. Optimize Third-Party Scripts

Load third-party scripts efficiently:

// ✅ Load third-party scripts asynchronously
function loadThirdPartyScript(src) {
const script = document.createElement("script");
script.src = src;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
// Load after page load
window.addEventListener("load", () => {
loadThirdPartyScript("https://example.com/analytics.js");
});
// ✅ Use intersection observer to load ads only when visible
const adObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadAd(entry.target);
adObserver.unobserve(entry.target);
}
});
});
adObserver.observe(document.querySelector(".ad-container"));

Measuring and Monitoring Performance

Accurately measuring performance is crucial for optimization. Here are the tools and techniques you need:

Lab Tools

Lab tools test performance in controlled environments:

Google PageSpeed Insights

Lighthouse

  • Built into Chrome DevTools
  • Provides comprehensive performance audit
  • Can be run programmatically
// Run Lighthouse programmatically
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
async function runLighthouse(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless"] });
const options = {
logLevel: "info",
output: "html",
onlyCategories: ["performance"],
port: chrome.port,
};
const runnerResult = await lighthouse(url, options);
await chrome.kill();
return runnerResult;
}

WebPageTest

  • Real browser testing from multiple locations
  • Filmstrip view and waterfall charts
  • Advanced testing options
  • URL: https://www.webpagetest.org/

Real User Monitoring (RUM)

Measure actual user experiences:

// ✅ Implement Web Vitals measurement
import { onLCP, onINP, onCLS } from "web-vitals";
function sendToAnalytics({ name, delta, value, id }) {
// Send to your analytics service
gtag("event", name, {
event_category: "Web Vitals",
value: Math.round(name === "CLS" ? value * 1000 : value),
event_label: id,
non_interaction: true,
});
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Performance Budgets

Set and enforce performance budgets:

performance-budget.js
// Example performance budget configuration
const budget = {
resources: [
{
resourceType: "script",
budget: 200, // 200KB
},
{
resourceType: "stylesheet",
budget: 100, // 100KB
},
{
resourceType: "image",
budget: 500, // 500KB
},
],
timings: [
{
metric: "largest-contentful-paint",
budget: 2500, // 2.5 seconds
},
{
metric: "interaction-to-next-paint",
budget: 200, // 200ms
},
{
metric: "cumulative-layout-shift",
budget: 0.1,
},
],
};

Real-World Optimization Examples

Let’s look at practical examples of optimizing real-world scenarios:

Example 1: Optimizing an E-commerce Product Page

<!-- ✅ Optimized product page -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
<!-- Preload critical resources -->
<link rel="preload" href="/critical.css" as="style" />
<link
rel="preload"
href="/hero-product.jpg"
as="image"
fetchpriority="high"
/>
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
body {
margin: 0;
font-family: system-ui;
}
.product-hero {
height: 60vh;
position: relative;
}
</style>
<!-- Load non-critical CSS asynchronously -->
<link
rel="stylesheet"
href="/styles.css"
media="print"
onload="this.media='all'"
/>
</head>
<body>
<!-- Product image with dimensions to prevent CLS -->
<div class="product-hero">
<picture>
<source srcset="product.avif" type="image/avif" />
<source srcset="product.webp" type="image/webp" />
<img
src="product.jpg"
alt="Product name"
width="1200"
height="800"
loading="eager"
fetchpriority="high"
decoding="async"
/>
</picture>
</div>
<!-- Product details -->
<main>
<h1>Product Name</h1>
<p>Product description...</p>
</main>
<!-- Load non-critical JavaScript -->
<script src="/analytics.js" defer></script>
</body>
</html>

Example 2: Optimizing a Blog Post Page

<!-- ✅ Optimized blog post -->
<article>
<!-- Hero image with proper dimensions -->
<header>
<img
src="blog-hero.jpg"
alt="Blog post title"
width="1200"
height="630"
loading="eager"
fetchpriority="high"
/>
<h1>Blog Post Title</h1>
</header>
<!-- Content with optimized images -->
<div class="content">
<p>Blog content...</p>
<!-- Lazy load images below the fold -->
<img
src="content-image.jpg"
alt="Content image"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
</div>
<!-- Related posts loaded on demand -->
<aside id="related-posts">
<!-- Skeleton loader to prevent CLS -->
<div class="skeleton-loader"></div>
</aside>
</article>
<script>
// Load related posts when visible
const relatedObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadRelatedPosts();
relatedObserver.unobserve(entry.target);
}
});
});
relatedObserver.observe(document.getElementById("related-posts"));
async function loadRelatedPosts() {
const response = await fetch("/api/related-posts");
const posts = await response.json();
// Render posts
}
</script>

Example 3: Optimizing JavaScript Bundle

// ✅ Code splitting with dynamic imports
// Instead of importing everything:
// import { Feature1, Feature2, Feature3 } from './features';
// Load features on demand:
const loadFeature = async (featureName) => {
try {
const module = await import(`./features/${featureName}.js`);
return module.default;
} catch (error) {
console.error(`Failed to load feature: ${featureName}`, error);
}
};
// Load feature when button is clicked
document
.getElementById("feature-button")
.addEventListener("click", async () => {
const Feature = await loadFeature("feature1");
if (Feature) {
new Feature().init();
}
});
// ✅ Use Web Workers for heavy computation
const worker = new Worker("data-processor.js");
function processLargeDataset(data) {
return new Promise((resolve, reject) => {
worker.postMessage({ data });
worker.onmessage = (event) => {
resolve(event.data);
};
worker.onerror = reject;
});
}

Performance Budgets and Best Practices

Setting Performance Budgets

Define clear performance budgets for your project:

.lighthouserc.json
{
"ci": {
"collect": {
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interaction-to-next-paint": ["error", { "maxNumericValue": 200 }],
"total-blocking-time": ["error", { "maxNumericValue": 300 }]
}
}
}
}

Best Practices Checklist

Image Optimization

  • Use modern formats (AVIF, WebP)
  • Specify width and height attributes
  • Implement lazy loading for below-fold images
  • Use responsive images with srcset
  • Optimize image quality and compression

JavaScript Optimization

  • Code split and lazy load non-critical code
  • Minimize and compress JavaScript
  • Defer non-critical scripts
  • Use Web Workers for heavy computation
  • Remove unused code (tree shaking)

CSS Optimization

  • Inline critical CSS
  • Defer non-critical CSS
  • Remove unused CSS
  • Minify CSS files
  • Use CSS containment

Font Optimization

  • Preload critical fonts
  • Use font-display: swap
  • Limit font variations
  • Subset fonts when possible
  • Use system fonts when appropriate

Caching

  • Set appropriate Cache-Control headers
  • Implement service workers
  • Use CDNs for static assets
  • Cache API responses when possible

Server Optimization

  • Optimize server response time (TTFB)
  • Enable compression (Gzip/Brotli)
  • Use HTTP/2 or HTTP/3
  • Implement proper caching headers
  • Optimize database queries

💡 Pro Tip: Regularly audit your performance using multiple tools. Lab tools like Lighthouse are great for development, but always validate with real user monitoring (RUM) data from tools like Google Analytics or specialized RUM services.


Conclusion

Web performance optimization is an ongoing process that requires continuous monitoring, testing, and refinement. Core Web Vitals provide clear, measurable goals for creating fast, responsive, and visually stable websites that both users and search engines love.

Remember these key takeaways:

  1. Core Web Vitals are ranking factors: Optimizing LCP, INP, and CLS directly impacts your SEO rankings and user experience.

  2. Measure real user experiences: Use both lab tools (Lighthouse, PageSpeed Insights) and real user monitoring (RUM) to get a complete picture of your performance.

  3. Optimize systematically: Start with the biggest wins—image optimization, code splitting, and eliminating render-blocking resources.

  4. Set performance budgets: Define clear performance goals and enforce them in your development workflow.

  5. Monitor continuously: Performance can degrade over time as you add features. Regular monitoring helps catch issues early.

By implementing the strategies and techniques covered in this guide, you’ll be well on your way to creating high-performing websites that rank well in search engines and provide excellent user experiences. Start with the quick wins, measure your progress, and iterate based on real user data.

For more optimization strategies, check out our guide on on-page SEO, which complements the performance techniques covered here.


Additional Resources: