Web Performance Optimization Cheatsheet
A comprehensive guide to web performance optimization - techniques for faster loading, rendering, and user interaction
Performance Metrics
Core Web Vitals
Google's key user experience metrics
# Largest Contentful Paint (LCP)
- Measures loading performance
- Good: <= 2.5 seconds
- Needs Improvement: <= 4 seconds
- Poor: > 4 seconds
- Affected by: server response time, render-blocking resources, resource load time, client-side rendering
# First Input Delay (FID)
- Measures interactivity
- Good: <= 100ms
- Needs Improvement: <= 300ms
- Poor: > 300ms
- Affected by: heavy JavaScript execution, long tasks, large JS bundles
# Cumulative Layout Shift (CLS)
- Measures visual stability
- Good: <= 0.1
- Needs Improvement: <= 0.25
- Poor: > 0.25
- Affected by: images without dimensions, ads, embeds, iframes, dynamically injected content
# Interaction to Next Paint (INP) - Replacing FID in March 2024
- Measures responsiveness
- Good: <= 200ms
- Needs Improvement: <= 500ms
- Poor: > 500ms
- Measured throughout page lifecycle (not just first interaction)
Other Important Metrics
Additional metrics for comprehensive performance evaluation
# Time to First Byte (TTFB)
- Time from request to first byte received
- Good: < 200ms
- Affected by: server processing, redirects, DNS lookup, connection setup
# First Contentful Paint (FCP)
- When first content appears on screen
- Good: <= 1.8 seconds
- Needs Improvement: <= 3 seconds
- Poor: > 3 seconds
# Total Blocking Time (TBT)
- Sum of all time between FCP and TTI where main thread was blocked
- Strong correlation with FID
- Good: < 200ms
# Time to Interactive (TTI)
- When page is fully interactive
- Good: <= 3.8 seconds
- Affected by: JavaScript size and execution time
# Speed Index
- How quickly content is visually displayed
- Good: <= 3.4 seconds
- Measures visual progression of page load
Measuring Performance
Tools and techniques for performance measurement
# Chrome DevTools Performance Panel
- Record runtime performance
- View main thread activity
- Identify bottlenecks
- Analyze rendering performance
- Access: F12 > Performance tab
# Lighthouse
- Open-source automated tool
- Audits performance, accessibility, SEO, etc.
- Access: Chrome DevTools > Lighthouse tab
- CLI: npm install -g lighthouse && lighthouse https://example.com
# WebPageTest.org
- Free performance testing tool
- Testing from multiple locations
- Waterfall charts, video, content breakdown
- Competitive analysis
# Chrome User Experience Report (CrUX)
- Real-user measurement (RUM) dataset
- Access via:
- PageSpeed Insights
- Search Console
- BigQuery
- CrUX API
# Performance API (JavaScript)
const perfEntries = performance.getEntriesByType('navigation');
const metrics = {
TTFB: perfEntries[0].responseStart - perfEntries[0].requestStart,
domLoad: perfEntries[0].domContentLoadedEventEnd,
fullPageLoad: perfEntries[0].loadEventEnd
};
# web-vitals.js library
import {getLCP, getFID, getCLS} from 'web-vitals';
getCLS(console.log); // CLS
getFID(console.log); // FID
getLCP(console.log); // LCP
Frontend Optimization
JavaScript Optimization
Techniques for optimizing JavaScript
# Code Splitting
// Before: One large bundle
import {bigFeature1, bigFeature2} from './big-module';
// After: Dynamic imports
const bigFeature1 = () => import('./big-feature-1');
const bigFeature2 = () => import('./big-feature-2');
# Tree Shaking
// Enable in webpack
// webpack.config.js
module.exports = {
mode: 'production', // Enables tree shaking
optimization: {
usedExports: true
}
};
# Avoid Render-Blocking JS
// Bad: Blocks parsing
<script src="app.js"></script>
// Better: Doesn't block parsing
<script src="app.js" defer></script>
// For critical JS: Loads in parallel and executes immediately
<script src="critical.js" async></script>
# Optimize Third-Party Scripts
// Load non-critical scripts after page load
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://third-party.com/widget.js';
document.body.appendChild(script);
});
# Web Workers for CPU-intensive tasks
// main.js
const worker = new Worker('worker.js');
worker.postMessage({data: complexData});
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = performComplexCalculation(e.data);
self.postMessage(result);
};
CSS Optimization
Strategies for CSS performance
# Critical CSS
// Inline critical CSS in <head>
<style>
/* Critical styles needed for above-the-fold content */
.header, .hero { /* styles */ }
</style>
// Load full CSS asynchronously
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
# Optimize CSS Selectors
// Inefficient (browsers read right-to-left)
body div header ul li a { color: red; }
// More efficient
.nav-link { color: red; }
# Minimize Repaints and Reflows
// Triggers reflow (bad for performance)
element.style.width = '100px'; // Read
console.log(element.offsetHeight); // Read - layout calculation
element.style.marginLeft = '10px'; // Write - causes reflow
// Better: Batch reads and writes
// Reads
const width = element.offsetWidth;
const height = element.offsetHeight;
// Then writes
element.style.width = '100px';
element.style.marginLeft = '10px';
# Use CSS Containment
.component {
contain: content; /* Isolates layout, style, paint containment */
}
# Reduce Unused CSS
// Use tools like PurgeCSS
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.js'],
})
]
}
Image Optimization
Best practices for image performance
# Modern Image Formats
- WebP: 25-35% smaller than JPEG with same quality
- AVIF: 50% smaller than JPEG with same quality
# Responsive Images
<picture>
<source srcset="image-large.webp 1200w, image-medium.webp 800w, image-small.webp 400w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
type="image/webp">
<source srcset="image-large.jpg 1200w, image-medium.jpg 800w, image-small.jpg 400w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
type="image/jpeg">
<img src="image-fallback.jpg" alt="Description" loading="lazy" width="800" height="600">
</picture>
# Lazy Loading
<!-- Native lazy loading -->
<img src="image.jpg" loading="lazy" alt="Description">
<!-- Intersection Observer API -->
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
</script>
# Image CDN Services
// Examples: Cloudinary, imgix, Cloudflare Images
// Original URL
https://example.com/images/photo.jpg
// CDN URL with transformations
https://example.cloudinary.com/f_auto,q_auto,w_800/photo.jpg
# Always Set Width & Height Attributes
<img src="image.jpg" width="800" height="600" alt="Description">
/* Prevents layout shifts when loading */
Backend & Network Optimization
Server Optimization
Techniques for server-side performance
# HTTP/2 Features
- Multiplexing: Multiple requests over one connection
- Server Push: Send resources before they're requested
- Header Compression: Reduces overhead
- Binary Protocol: More efficient than text
# Enable HTTP/2 on Nginx
server {
listen 443 ssl http2;
# SSL configuration
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# ...
}
# Enable HTTP/2 on Apache
<VirtualHost *:443>
Protocols h2 http/1.1
# SSL configuration
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
# ...
</VirtualHost>
# Server Push (HTTP/2)
// In Node.js
app.get('/', (req, res) => {
const stream = res.push('/app.js', {
request: { accept: '**/*' },
response: { 'content-type': 'application/javascript' }
});
stream.end('console.log("pushed!");');
res.end('<html><script src="/app.js"></script></html>');
});
# Server-Side Caching
// Redis caching example (pseudocode)
// Initialize Redis client
const client = createRedisClient();
// Store data with expiration (1 hour)
client.set("key", "value", "EX", 3600);
// Get cached data
const value = client.get("key");
// Caching pattern for database queries
function getUserWithCache(id) {
// Check cache first
const cachedUser = client.get("user:" + id);
if (cachedUser) return JSON.parse(cachedUser);
// Get from database if not in cache
const user = database.getUser(id);
// Save in cache for future requests
client.set("user:" + id, JSON.stringify(user), "EX", 3600);
return user;
}
Caching Strategies
Effective caching techniques for web performance
# Browser Cache Headers
# Cache-Control (modern)
Cache-Control: max-age=31536000, immutable # For versioned resources
Cache-Control: no-cache # Force revalidation
Cache-Control: private, max-age=3600 # Private with expiration
Cache-Control: public, max-age=86400 # Shared cache, 1 day
# ETag for validation
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# Last-Modified for validation
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
# Service Worker Caching
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
# CDN Caching
// Cloudflare example - Page Rule settings
URL: example.com/*
Cache Level: Cache Everything
Edge Cache TTL: 1 month
Browser Cache TTL: 1 month
Resource Hints
Optimize resource loading with browser hints
# Preload: Highest priority, load ASAP
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="important.js" as="script">
<link rel="preload" href="hero.webp" as="image">
<link rel="preload" href="font.woff2" as="font" crossorigin>
# Prefetch: Lower priority, for future pages
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/next-page.js">
# Preconnect: Establish early connections
<link rel="preconnect" href="https://cdn.example.com">
# DNS-Prefetch: Resolve DNS early
<link rel="dns-prefetch" href="https://api.example.com">
# Prerender: Full preload of a page (use carefully)
<link rel="prerender" href="https://example.com/next-likely-page">
# Prioritizing Critical Resources
<!-- Highest priority -->
<link rel="preload" href="critical.css" as="style">
<link rel="stylesheet" href="critical.css">
<!-- Medium priority -->
<link rel="preconnect" href="https://third-party.com">
<!-- Lower priority -->
<link rel="prefetch" href="next-page.js">
User Perception Techniques
Perceived Performance
Making your app feel faster to users
# Skeleton Screens
<div class="skeleton-card">
<div class="skeleton-image"></div>
<div class="skeleton-title"></div>
<div class="skeleton-text"></div>
</div>
.skeleton-card {
padding: 20px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.skeleton-image {
height: 200px;
background: linear-gradient(90deg, #f0f0f0, #e0e0e0, #f0f0f0);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
# Progressive Rendering
<!-- First, load minimal critical HTML -->
<!DOCTYPE html>
<html>
<head>
<style>/* Critical CSS */</style>
</head>
<body>
<header><!-- Critical content --></header>
<main id="content">Loading...</main>
<script>
// Then, fetch and render the rest
fetch('/api/content')
.then(res => res.json())
.then(data => {
document.getElementById('content').innerHTML =
renderContent(data);
});
</script>
</body>
</html>
# Optimistic UI
// In a React app
function TodoApp() {
const [todos, setTodos] = useState([]);
const addTodo = async (text) => {
// Optimistically add to UI
const newTodo = { id: Date.now(), text, completed: false };
setTodos([...todos, newTodo]);
try {
// Actually save to server
const savedTodo = await api.createTodo(text);
// Update with real ID from server
setTodos(todos =>
todos.map(t => t.id === newTodo.id ? savedTodo : t)
);
} catch (error) {
// Revert on error
setTodos(todos => todos.filter(t => t.id !== newTodo.id));
alert('Failed to add todo');
}
};
// ...
}
Progressive Web App (PWA)
Make your web app installable and offline-capable
# Web App Manifest
// manifest.json
{
"name": "My PWA App",
"short_name": "PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
# Link manifest in HTML
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4285f4">
# Basic Service Worker for Offline
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
];
// Install and cache
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Serve from cache, fall back to network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) return response;
// Try network
return fetch(event.request)
.then(response => {
// Don't cache if not valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Cache the new resource
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Offline fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
# Register Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(err => {
console.log('SW registration failed:', err);
});
});
}
Performance Budgets
Setting and enforcing performance limits
# Types of Performance Budgets
- Quantity-based: Number of resources, file size
- Rule-based: Lighthouse scores, Core Web Vitals thresholds
- Time-based: Time to specific events (FCP, TTI)
# Example Budget
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 170 // kB
},
{
"resourceType": "total",
"budget": 500 // kB
}
],
"timings": [
{
"metric": "largest-contentful-paint",
"budget": 2500 // ms
},
{
"metric": "cumulative-layout-shift",
"budget": 0.1
}
]
}
# Webpack Performance Budget
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 100000, // 100 kB
maxEntrypointSize: 250000, // 250 kB
hints: 'error' // 'warning' or false are other options
}
};
# Automated CI Testing
// Example Lighthouse CI configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['https://example.com/'],
numberOfRuns: 3
},
assert: {
assertions: {
'categories:performance': ['error', {minScore: 0.9}],
'largest-contentful-paint': ['error', {maxNumericValue: 2500}],
'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}]
}
},
upload: {
target: 'temporary-public-storage'
}
}
};