Web Performance Optimization - Core Web Vitals and Beyond
Web performance directly impacts user experience, SEO rankings, and business metrics. Google's Core Web Vitals have made performance optimization more crucial than ever. This comprehensive guide covers modern techniques for building lightning-fast web applications.
Understanding Core Web Vitals
1. Largest Contentful Paint (LCP)
Measures loading performance - should occur within 2.5 seconds:
// Monitor LCP with Web Vitals library
import { getLCP } from "web-vitals";
getLCP((metric) => {
console.log("LCP:", metric.value);
// Send to analytics
gtag("event", "web_vitals", {
event_category: "Web Vitals",
event_action: "LCP",
value: Math.round(metric.value),
non_interaction: true,
});
});
// Optimize LCP with resource hints
const preloadCriticalResources = () => {
// Preload hero image
const heroImage = new Image();
heroImage.src = "/images/hero-optimized.webp";
// Preload critical fonts
const fontLink = document.createElement("link");
fontLink.rel = "preload";
fontLink.href = "/fonts/inter-var.woff2";
fontLink.as = "font";
fontLink.type = "font/woff2";
fontLink.crossOrigin = "anonymous";
document.head.appendChild(fontLink);
};
2. First Input Delay (FID) / Interaction to Next Paint (INP)
Measures interactivity - FID should be less than 100ms, INP less than 200ms:
// Monitor FID and INP
import { getFID, getINP } from "web-vitals";
getFID((metric) => {
console.log("FID:", metric.value);
});
getINP((metric) => {
console.log("INP:", metric.value);
});
// Optimize with code splitting and lazy loading
const LazyComponent = React.lazy(() =>
import("./HeavyComponent").then((module) => ({
default: module.HeavyComponent,
}))
);
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
const memoizedValue = useMemo(() => {
return heavyComputation(data);
}, [data]);
return <div>{memoizedValue}</div>;
});
// Debounce user inputs
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
3. Cumulative Layout Shift (CLS)
Measures visual stability - should be less than 0.1:
/* Prevent layout shifts with aspect ratios */
.responsive-image {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* Maintain space before image loads */
}
/* Reserve space for dynamic content */
.ad-container {
min-height: 300px; /* Reserve space for ads */
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.skeleton-loader {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
// Monitor CLS
import { getCLS } from "web-vitals";
getCLS((metric) => {
console.log("CLS:", metric.value);
});
// Prevent CLS with proper image sizing
const ResponsiveImage = ({ src, alt, width, height, ...props }) => {
const [loaded, setLoaded] = useState(false);
return (
<div
style={{
aspectRatio: `${width} / ${height}`,
background: loaded ? "transparent" : "#f0f0f0",
}}
>
<img
src={src}
alt={alt}
width={width}
height={height}
onLoad={() => setLoaded(true)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
opacity: loaded ? 1 : 0,
transition: "opacity 0.3s ease",
}}
{...props}
/>
</div>
);
};
Advanced Loading Strategies
1. Critical Resource Prioritization
<!DOCTYPE html>
<html>
<head>
<!-- Preconnect to external domains -->
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://api.example.com"
/>
<!-- Preload critical resources -->
<link
rel="preload"
href="/css/critical.css"
as="style"
/>
<link
rel="preload"
href="/js/app.js"
as="script"
/>
<!-- Critical CSS inlined -->
<style>
/* Above-the-fold styles here */
.hero {
/* ... */
}
.navigation {
/* ... */
}
</style>
<!-- Non-critical CSS loaded asynchronously -->
<link
rel="preload"
href="/css/non-critical.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript
><link
rel="stylesheet"
href="/css/non-critical.css"
/></noscript>
</head>
</html>
2. Image Optimization Strategies
// Next.js Image component with optimization
import Image from "next/image";
const OptimizedImage = ({ src, alt, ...props }) => (
<Image
src={src}
alt={alt}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
quality={75}
formats={["webp", "avif"]}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
{...props}
/>
);
// Progressive image loading
const ProgressiveImage = ({ src, placeholder, alt }) => {
const [currentSrc, setCurrentSrc] = useState(placeholder);
const [loading, setLoading] = useState(true);
useEffect(() => {
const imageToLoad = new Image();
imageToLoad.src = src;
imageToLoad.onload = () => {
setCurrentSrc(src);
setLoading(false);
};
}, [src]);
return (
<img
src={currentSrc}
alt={alt}
className={`progressive-image ${loading ? "loading" : "loaded"}`}
/>
);
};
3. Code Splitting and Lazy Loading
// Route-based code splitting
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
const App = () => (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/about"
element={<About />}
/>
<Route
path="/dashboard"
element={<Dashboard />}
/>
</Routes>
</Suspense>
</BrowserRouter>
);
// Component-based lazy loading with Intersection Observer
const LazySection = ({ children, threshold = 0.1 }) => {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [threshold]);
return <div ref={ref}>{isVisible ? children : <div style={{ height: "200px" }} />}</div>;
};
JavaScript Performance Optimization
1. Bundle Analysis and Optimization
// Webpack Bundle Analyzer configuration
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
reportFilename: "bundle-report.html",
}),
],
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
common: {
name: "common",
minChunks: 2,
chunks: "all",
},
},
},
},
};
// Tree shaking optimization
// Import only what you need
import { debounce } from "lodash"; // ❌ Imports entire lodash
import debounce from "lodash/debounce"; // ✅ Imports only debounce
// Use ES modules for better tree shaking
export const utils = {
formatDate: (date) => {
/* ... */
},
formatCurrency: (amount) => {
/* ... */
},
};
// Instead of exporting everything as default
export { formatDate, formatCurrency };
2. Memory Management and Leak Prevention
// Proper cleanup in React components
const useEventListener = (eventName, handler, element = window) => {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
};
// Cleanup timers and intervals
const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}
}, [delay]);
};
// Abort fetch requests on unmount
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const abortController = new AbortController();
fetch(url, { signal: abortController.signal })
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
if (error.name !== "AbortError") {
console.error("Fetch error:", error);
}
});
return () => abortController.abort();
}, [url]);
return { data, loading };
};
CSS Performance Optimization
1. Critical CSS and Loading Strategies
/* Critical CSS - inline in HTML head */
/* Above-the-fold styles only */
.header {
position: fixed;
top: 0;
width: 100%;
background: white;
z-index: 1000;
}
.hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* Use CSS containment for performance */
.widget {
contain: layout style paint;
}
.article-content {
contain: layout;
}
/* Optimize animations */
.smooth-animation {
transform: translateX(0);
transition: transform 0.3s ease;
will-change: transform; /* Hint to browser for optimization */
}
/* Remove will-change after animation */
.animation-complete {
will-change: auto;
}
2. Advanced CSS Optimization
/* Use CSS Grid for efficient layouts */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
/* More efficient than flexbox for 2D layouts */
}
/* Optimize font loading */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap; /* Improve font loading performance */
}
/* Use custom properties for better performance */
:root {
--primary-color: #007acc;
--text-color: #333;
--spacing-unit: 1rem;
}
/* Avoid expensive CSS operations */
.optimized-shadow {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Simpler than complex shadows */
}
/* Use transform instead of changing layout properties */
.move-element {
transform: translateX(100px); /* ✅ Composited */
/* left: 100px; ❌ Causes layout */
}
Monitoring and Analytics
1. Real User Monitoring (RUM)
// Comprehensive performance monitoring
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// Core Web Vitals
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(this.sendMetric.bind(this));
getFID(this.sendMetric.bind(this));
getFCP(this.sendMetric.bind(this));
getLCP(this.sendMetric.bind(this));
getTTFB(this.sendMetric.bind(this));
});
// Custom metrics
this.measureCustomMetrics();
this.setupErrorTracking();
}
sendMetric(metric) {
// Send to analytics service
fetch("/api/metrics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
}),
});
}
measureCustomMetrics() {
// Measure time to interactive
const measureTTI = () => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const tti = entries[entries.length - 1].startTime;
this.sendMetric({ name: "TTI", value: tti });
});
observer.observe({ entryTypes: ["longtask"] });
};
// Measure resource loading times
const measureResources = () => {
const resources = performance.getEntriesByType("resource");
resources.forEach((resource) => {
if (resource.duration > 1000) {
// Flag slow resources
this.sendMetric({
name: "slow_resource",
value: resource.duration,
resource: resource.name,
});
}
});
};
measureTTI();
measureResources();
}
setupErrorTracking() {
window.addEventListener("error", (event) => {
this.sendMetric({
name: "javascript_error",
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
window.addEventListener("unhandledrejection", (event) => {
this.sendMetric({
name: "promise_rejection",
reason: event.reason,
});
});
}
}
// Initialize monitoring
const monitor = new PerformanceMonitor();
2. Performance Budget Implementation
// Performance budget checker
const performanceBudget = {
metrics: {
LCP: 2500,
FID: 100,
CLS: 0.1,
TTI: 3800,
bundleSize: 250000, // 250KB
},
resources: {
images: 500000, // 500KB
fonts: 100000, // 100KB
css: 50000, // 50KB
js: 200000, // 200KB
},
};
const checkBudget = () => {
// Check bundle size
fetch("/api/bundle-size")
.then((response) => response.json())
.then((data) => {
if (data.size > performanceBudget.metrics.bundleSize) {
console.warn(`Bundle size exceeded: ${data.size} bytes`);
// Alert team or fail build
}
});
// Check resource sizes
const resources = performance.getEntriesByType("resource");
const resourceSizes = resources.reduce((acc, resource) => {
const type = getResourceType(resource.name);
acc[type] = (acc[type] || 0) + resource.transferSize;
return acc;
}, {});
Object.entries(resourceSizes).forEach(([type, size]) => {
if (performanceBudget.resources[type] && size > performanceBudget.resources[type]) {
console.warn(`${type} budget exceeded: ${size} bytes`);
}
});
};
const getResourceType = (url) => {
if (/\.(jpe?g|png|gif|webp|avif)$/i.test(url)) return "images";
if (/\.(woff2?|ttf|eot)$/i.test(url)) return "fonts";
if (/\.css$/i.test(url)) return "css";
if (/\.js$/i.test(url)) return "js";
return "other";
};
Conclusion
Web performance optimization is an ongoing process that requires continuous monitoring and improvement. Key takeaways:
- Focus on Core Web Vitals as primary metrics for user experience
- Implement comprehensive monitoring to identify performance bottlenecks
- Use modern loading strategies like lazy loading and code splitting
- Optimize images and fonts which often constitute the largest resources
- Set and monitor performance budgets to prevent regression
- Test on real devices and network conditions your users experience
Remember: Performance is a feature, not an afterthought. Users expect fast, responsive web experiences, and search engines reward sites that deliver them.