Web Performance Optimization: Speed Up Your Site
· 12 min read
Table of Contents
- Core Web Vitals Explained
- Image Optimization Strategies
- Implementing Lazy Loading
- Code Minification and Bundling
- Advanced Caching Strategies
- CDN Configuration Best Practices
- Optimizing the Critical Rendering Path
- Resource Hints and Preloading
- JavaScript Performance Optimization
- Performance Monitoring and Metrics
- Performance Audit Checklist
- Frequently Asked Questions
Website performance isn't just about making your site feel faster—it directly impacts your bottom line. Studies show that a one-second delay in page load time can reduce conversions by 7%, and 53% of mobile users abandon sites that take longer than three seconds to load.
In this comprehensive guide, we'll walk through proven strategies to optimize your website's performance, from understanding Core Web Vitals to implementing advanced caching techniques. Whether you're running an e-commerce store, a content site, or a web application, these techniques will help you deliver a faster, more responsive experience to your users.
Core Web Vitals Explained
Core Web Vitals are Google's standardized metrics for measuring user experience on the web. Since 2021, they've been a ranking factor in Google's search algorithm, making them essential for both SEO and user satisfaction.
These metrics focus on three critical aspects of user experience: loading performance, interactivity, and visual stability. Let's break down each metric and explore practical optimization strategies.
LCP — Largest Contentful Paint
Target: under 2.5 seconds
LCP measures how long it takes for the largest visible content element to render on screen. This is typically your hero image, main heading, or video player—whatever dominates the viewport when the page first loads.
Common culprits that slow down LCP include:
- Slow server response times (high TTFB)
- Render-blocking JavaScript and CSS
- Large, unoptimized images
- Client-side rendering that delays content
Optimization strategies:
- Preload critical resources: Tell the browser to fetch your LCP element immediately
<link rel="preload" as="image" href="hero.webp">
<link rel="preload" as="font" href="main-font.woff2" crossorigin>
- Optimize server response time: Aim for TTFB under 600ms by using faster hosting, implementing server-side caching, and optimizing database queries
- Use a CDN: Serve static assets from edge locations closer to your users
- Eliminate render-blocking resources: Inline critical CSS and defer non-critical JavaScript
- Optimize images: Use modern formats like WebP or AVIF, compress aggressively, and serve responsive images
Pro tip: Use the Lighthouse Analyzer to identify your LCP element and see exactly what's blocking it from loading quickly.
INP — Interaction to Next Paint
Target: under 200 milliseconds
INP replaced First Input Delay (FID) in 2024 as a more comprehensive measure of responsiveness. It tracks the latency of all user interactions throughout the page lifecycle—clicks, taps, and keyboard inputs.
Poor INP scores usually stem from:
- Long-running JavaScript tasks blocking the main thread
- Heavy event handlers that take too long to execute
- Excessive DOM manipulation
- Third-party scripts monopolizing CPU time
Optimization strategies:
- Break up long tasks: Any JavaScript task over 50ms should be split into smaller chunks
// Instead of processing everything at once
function processItems(items) {
items.forEach(item => heavyOperation(item));
}
// Break it into chunks
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
heavyOperation(items[i]);
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
- Use Web Workers: Offload heavy computation to background threads
- Debounce expensive handlers: Limit how often event handlers fire during rapid user input
- Optimize third-party scripts: Load them asynchronously and consider using a tag manager to control execution
- Use requestIdleCallback: Schedule non-critical work during browser idle time
CLS — Cumulative Layout Shift
Target: under 0.1
CLS measures visual stability by tracking unexpected layout shifts during page load. Nothing frustrates users more than clicking a button only to have it move at the last second because an ad loaded above it.
Common causes of layout shift:
- Images and videos without dimensions
- Dynamically injected content (ads, embeds)
- Web fonts causing FOIT/FOUT
- Animations that trigger layout recalculation
Optimization strategies:
- Always specify dimensions: Set explicit width and height attributes on all media
<img src="product.jpg" width="800" height="600" alt="Product">
<video width="1920" height="1080" poster="thumbnail.jpg">
- Reserve space for dynamic content: Use CSS aspect-ratio or min-height
.ad-container {
min-height: 250px;
aspect-ratio: 16 / 9;
}
- Preload fonts: Prevent font swapping from causing layout shifts
- Use CSS containment: Isolate layout changes to specific elements
- Avoid inserting content above existing content: Add new elements below the fold or use overlays
Image Optimization Strategies
Images typically account for 50-70% of a page's total weight, making them the single biggest opportunity for performance gains. Modern image optimization goes far beyond just compressing JPEGs.
Choosing the Right Format
Different image formats excel at different use cases. Here's a comprehensive comparison:
| Format | Best For | Compression | Browser Support |
|---|---|---|---|
| WebP | Photos, complex graphics | 25-35% smaller than JPEG | 96% (all modern browsers) |
| AVIF | Photos, high-quality images | 50% smaller than JPEG | 88% (Chrome, Firefox, Safari 16+) |
| JPEG | Fallback for photos | Baseline compression | 100% |
| PNG | Transparency, simple graphics | Lossless, larger files | 100% |
| SVG | Icons, logos, illustrations | Scalable, very small | 100% |
Use the <picture> element to serve modern formats with fallbacks:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" width="1200" height="600">
</picture>
Responsive Images
Serving the same 2000px image to mobile users is wasteful. Use srcset and sizes to let browsers choose the optimal image size:
<img
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
src="medium.jpg"
alt="Responsive image">
This tells the browser: "I have three versions available. On screens up to 600px wide, use the 400px version. On screens up to 1000px, use 800px. Otherwise, use 1200px."
Compression Techniques
Aggressive compression can reduce file sizes by 60-80% with minimal quality loss:
- JPEG: Use quality 80-85 for most photos (quality 90+ is rarely perceptible)
- PNG: Run through tools like pngquant or TinyPNG to reduce color palettes
- WebP: Use quality 75-80 for lossy compression
- AVIF: Use quality 60-70 (AVIF's compression is more efficient)
Try the Image Optimizer to batch-process and compare different formats and quality settings.
Quick tip: Enable "Save-Data" mode detection to serve even more compressed images to users on slow connections: if (navigator.connection?.saveData) { /* serve lower quality */ }
Implementing Lazy Loading
Lazy loading defers loading of off-screen resources until they're needed, dramatically reducing initial page weight and improving load times.
Native Lazy Loading
Modern browsers support native lazy loading with a simple attribute:
<img src="image.jpg" loading="lazy" alt="Description">
<iframe src="embed.html" loading="lazy"></iframe>
This works for images and iframes with 95%+ browser support. The browser automatically loads resources as they approach the viewport.
When to use eager loading:
- Above-the-fold images (especially LCP elements)
- Critical UI elements
- Small images that don't impact performance
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero">
JavaScript-Based Lazy Loading
For more control or older browser support, use the Intersection Observer API:
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
HTML structure:
<img data-src="actual-image.jpg" src="placeholder.jpg" class="lazy" alt="Description">
Lazy Loading Best Practices
- Use placeholders: Show low-quality image placeholders (LQIP) or solid colors to prevent layout shift
- Set appropriate thresholds: Start loading images 200-300px before they enter the viewport
- Lazy load third-party embeds: YouTube videos, social media widgets, and maps are heavy—load them on interaction
- Don't lazy load everything: Above-the-fold content should load immediately
Code Minification and Bundling
Minification removes unnecessary characters from code without changing functionality, while bundling combines multiple files to reduce HTTP requests.
CSS Optimization
CSS files can be surprisingly large, especially when using frameworks. Here's how to optimize them:
- Minify CSS: Remove whitespace, comments, and redundant code
- Remove unused CSS: Tools like PurgeCSS eliminate styles you're not using
- Critical CSS: Inline above-the-fold styles and defer the rest
<style>
/* Critical CSS inlined here */
.header { background: #38bdf8; }
.hero { min-height: 400px; }
</style>
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="main.css"></noscript>
JavaScript Optimization
JavaScript is the most expensive resource to process—it must be downloaded, parsed, compiled, and executed.
Minification strategies:
- Use tools like Terser or esbuild to minify JavaScript
- Enable tree-shaking to remove dead code
- Split code into chunks and load them on demand
Code splitting example with dynamic imports:
// Instead of importing everything upfront
import { heavyLibrary } from './heavy-library.js';
// Load it only when needed
button.addEventListener('click', async () => {
const { heavyLibrary } = await import('./heavy-library.js');
heavyLibrary.doSomething();
});
Build Tool Configuration
Modern build tools handle minification automatically. Here's a sample Vite configuration:
// vite.config.js
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns']
}
}
}
}
}
Pro tip: Use the Bundle Analyzer to visualize your JavaScript bundles and identify opportunities for code splitting.
Advanced Caching Strategies
Caching is the practice of storing copies of resources so future requests can be served faster. Effective caching can reduce server load by 80-90% and dramatically improve repeat visit performance.
Browser Caching
Control how browsers cache your resources using HTTP headers:
| Header | Purpose | Example |
|---|---|---|
Cache-Control |
Primary caching directive | max-age=31536000, immutable |
ETag |
Validation token for conditional requests | W/"686897696a7c876b7e" |
Last-Modified |
When resource was last changed | Wed, 21 Oct 2025 07:28:00 GMT |
Expires |
Legacy expiration date (use Cache-Control instead) | Thu, 01 Dec 2026 16:00:00 GMT |
Recommended caching strategy:
# Static assets with hashed filenames (immutable)
Cache-Control: public, max-age=31536000, immutable
# HTML pages (always revalidate)
Cache-Control: no-cache
# API responses (short cache)
Cache-Control: public, max-age=300, must-revalidate
Service Worker Caching
Service workers enable sophisticated offline-first caching strategies:
// service-worker.js
const CACHE_NAME = 'v1';
const STATIC_ASSETS = ['/css/main.css', '/js/app.js', '/images/logo.svg'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
CDN Caching
CDNs cache content at edge locations worldwide. Configure cache behavior with headers:
- Cache everything: Static assets, images, fonts
- Cache with short TTL: API responses, frequently updated content
- Don't cache: Personalized content, authenticated pages, POST requests
Use Vary headers to cache different versions based on request headers:
Vary: Accept-Encoding, Accept
CDN Configuration Best Practices
Content Delivery Networks distribute your content across global edge servers, reducing latency by serving files from locations closer to your users.
Choosing a CDN
Popular CDN options include:
- Cloudflare: Free tier available, excellent DDoS protection, 300+ locations
- AWS CloudFront: Deep AWS integration, pay-as-you-go pricing
- Fastly: Real-time purging, advanced edge computing
- Bunny CDN: Cost-effective, simple pricing
CDN Configuration Steps
- Set up origin server: Configure your web server as the CDN origin
- Configure cache rules: Define what to cache and for how long
- Enable compression: Gzip or Brotli compression at the edge
- Set up SSL/TLS: Enable HTTPS with automatic certificate management
- Configure purging: Set up cache invalidation for content updates
Advanced CDN Features
Edge computing: Run code at CDN edge locations for dynamic content
// Cloudflare Worker example
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// Serve WebP to supporting browsers
if (request.headers.get('Accept')?.includes('image/webp')) {
url.pathname = url.pathname.replace(/\.(jpg|png)$/, '.webp');
}
return fetch(url);
}
Image optimization: Many CDNs offer automatic image optimization and resizing
<!-- Cloudflare Image Resizing -->
<img src="/cdn-cgi/image/width=800,quality=85/image.jpg" alt="Optimized">
Pro tip: Use the CDN Performance Tester to compare response times from different CDN providers across global locations.
Optimizing the Critical Rendering Path
The critical rendering path is the sequence of steps browsers take to convert HTML, CSS, and JavaScript into pixels on screen. Optimizing this path is key to fast initial renders.
Understanding the Rendering Process
- DOM Construction: Browser parses HTML into a Document Object Model
- CSSOM Construction: CSS is parsed into a CSS Object Model
- Render Tree: DOM and CSSOM combine to create the render tree
- Layout: Browser calculates position and size of elements
- Paint: Pixels are drawn to screen
Eliminating Render-Blocking Resources
CSS and JavaScript can block rendering. Here's how to minimize their impact:
CSS optimization:
- Inline critical CSS in the
<head> - Load non-critical CSS asynchronously
- Use media queries to mark CSS as non-render-blocking
<!-- Render-blocking (necessary for above-the-fold) -->
<link rel="stylesheet" href="critical.css">
<!-- Non-blocking (for print styles) -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Async loading -->
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'">
JavaScript optimization:
- Place scripts at the end of
<body> - Use
asyncfor independent scripts - Use
deferfor scripts that need DOM access
<!-- Blocks parsing and rendering -->
<script src="blocking.js"></script>
<!-- Downloads in parallel, executes when ready -->
<script src="analytics.js" async></script>
<!-- Downloads in parallel, executes after DOM ready -->
<script src="app.js" defer></script>
Reducing Critical Resources
Minimize the number of resources needed for initial render:
- Inline small CSS and JavaScript (under 14KB)
- Remove unused code with tree-shaking
- Defer third-party scripts
- Use system fonts or preload custom fonts
Resource Hints and Preloading
Resource hints tell browsers about resources they'll need, enabling smarter loading decisions.
DNS Prefetch
Resolve DNS for external domains early:
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
Preconnect
Establish early connections to important third-party origins:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Preconnect performs DNS lookup, TCP handshake, and TLS negotiation.
Prefetch
Download resources that will be needed on future navigations:
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/images/next-hero.jpg">
Preload
High-priority loading for critical resources:
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="critical.css" as="style">
Use fetchpriority to fine-tune loading priority:
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<img src="footer-logo.jpg" fetchpriority="low" alt="Logo">
JavaScript Performance Optimization
JavaScript is the most expensive resource to process. Every kilobyte of JavaScript requires downloading, parsing, compiling, and executing—all on the main thread.
Reducing JavaScript Payload
Code splitting: Break large bundles into smaller chunks
// Route-based splitting with React
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
Tree shaking: Eliminate dead code during build
// Instead of importing everything
import _ from 'lodash';
// Import only what you need
import debounce from 'lodash/debounce';
Optimizing JavaScript Execution
Avoid long tasks: Break work into smaller chunks to keep the main thread responsive
async function processLargeDataset(items) {
const chunkSize = 100;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
// Process chunk
chunk.forEach(item => processItem(item));
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Use Web Workers: Offload heavy computation to background threads
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
Third-Party Script Management
Third-party scripts are a major performance bottleneck. Manage them carefully: