Web Performance Optimisation
Comprehensive guide to optimising web application performance across loading, rendering, and runtime.
Web Performance Optimisation
Comprehensive guide to optimising web application performance across loading, rendering, and runtime.
Overview
Web performance optimisation focuses on reducing page load times, improving render performance, and enhancing user experience through efficient resource delivery and code execution. Key metrics include First Contentful Paint (FCP), Largest Contentful Paint (LCP), Time to Interactive (TTI), and Cumulative Layout Shift (CLS).
flowchart TB
A[User Request] --> B[DNS Lookup]
B --> C[TCP Connection]
C --> D[Server Processing]
D --> E[Resource Download]
E --> F{Resource Type}
F -->|HTML| G[Parse HTML]
F -->|CSS| H[Parse CSS]
F -->|JavaScript| I[Parse & Execute JS]
G --> J[Build DOM]
H --> K[Build CSSOM]
J --> L[Render Tree]
K --> L
I --> L
L --> M[Layout]
M --> N[Paint]
N --> O[Composite]
O --> P[First Contentful Paint]
Critical Rendering Path
The critical rendering path is the sequence of steps browsers take to convert HTML, CSS, and JavaScript into rendered pixels.
Understanding the Rendering Pipeline
sequenceDiagram
participant Browser
participant HTML
participant CSS
participant JS
participant Render
Browser->>HTML: Request & Parse
HTML-->>Browser: DOM Tree
Browser->>CSS: Request & Parse
CSS-->>Browser: CSSOM Tree
Browser->>JS: Request & Execute
JS-->>Browser: DOM/CSSOM Updates
Browser->>Render: Combine DOM + CSSOM
Render-->>Browser: Render Tree
Browser->>Browser: Layout
Browser->>Browser: Paint
Browser->>Browser: Composite
Optimising Critical Path
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
.hero { background: #333; color: #fff; padding: 2rem; }
.header { position: fixed; top: 0; width: 100%; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
<!-- Async non-critical JavaScript -->
<script src="/scripts/analytics.js" async></script>
<!-- Defer JavaScript that modifies DOM -->
<script src="/scripts/app.js" defer></script>
Resource Prioritisation
<!-- Preconnect to required origins -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- DNS prefetch for future navigation -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">
<!-- Prefetch resources for next page -->
<link rel="prefetch" href="/page2.html">
<!-- Preload an ES module and its dependency graph -->
<link rel="modulepreload" href="/scripts/app.mjs">
<!-- Prerender a likely next page (use the Speculation Rules API; -->
<!-- <link rel="prerender"> is deprecated and Chrome-only) -->
<script type="speculationrules">
{
"prerender": [{ "urls": ["/checkout"] }]
}
</script>
Caching Strategies
Effective caching reduces server load and improves perceived performance.
Browser Caching
# nginx configuration
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~* \.(html)$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location /api/ {
expires -1;
add_header Cache-Control "no-store, private";
}
Cache-Control Headers
// Express.js examples
const express = require('express');
const app = express();
// Static assets with versioning
app.use('/static', express.static('public', {
maxAge: '365d',
immutable: true
}));
// API responses with ETag
app.get('/api/data', (req, res) => {
const data = getData();
const etag = generateETag(data);
res.set({
'Cache-Control': 'public, max-age=300, must-revalidate',
'ETag': etag
});
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(data);
});
// HTML pages with validation
app.get('/', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=0, must-revalidate',
'Last-Modified': new Date().toUTCString()
});
res.sendFile('index.html');
});
Service Worker Caching
// service-worker.js
const CACHE_VERSION = 'v1';
const CACHE_STATIC = `static-${CACHE_VERSION}`;
const CACHE_DYNAMIC = `dynamic-${CACHE_VERSION}`;
// Install event - cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_STATIC).then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.svg',
'/offline.html'
]);
})
);
});
// Activate event - clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_STATIC && key !== CACHE_DYNAMIC)
.map(key => caches.delete(key))
);
})
);
});
// Fetch event - network first, falling back to cache
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
// Network first for API calls
event.respondWith(
fetch(event.request)
.then(response => {
const responseClone = response.clone();
caches.open(CACHE_DYNAMIC).then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
} else {
// Cache first for static assets
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
}
});
CDN Configuration
// CloudFront cache behaviour example
{
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": ["GET", "HEAD", "OPTIONS"],
"CachedMethods": ["GET", "HEAD"],
"Compress": true,
"DefaultTTL": 86400,
"MaxTTL": 31536000,
"MinTTL": 0,
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
},
"Headers": ["Origin", "Access-Control-Request-Method"]
}
}
flowchart LR
A[User] --> B{CDN Cache}
B -->|Hit| C[Return Cached]
B -->|Miss| D[Origin Server]
D --> E[Process Request]
E --> F[Return Response]
F --> G[Cache at CDN]
G --> A
C --> A
Minification and Bundling
Reduce file sizes and HTTP requests through minification and bundling.
JavaScript Minification
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
mangle: {
safari10: true
},
output: {
comments: false,
ascii_only: true
}
},
extractComments: false
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
CSS Optimisation
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: {
removeAll: true
},
normalizeWhitespace: true,
colormin: true,
minifyFontValues: true,
minifySelectors: true
}]
}),
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.js'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
};
Tree Shaking
// package.json
{
"sideEffects": [
"*.css",
"*.scss"
]
}
// Import only what you need
import { debounce, throttle } from 'lodash-es'; // Good
// import _ from 'lodash'; // Bad - imports everything
// ES6 modules for tree shaking
export const util1 = () => {};
export const util2 = () => {};
// Use named imports
import { util1 } from './utils'; // Good
// import * as utils from './utils'; // Bad
Code Splitting
// Dynamic imports for route-based splitting
const Home = () => import('./pages/Home.vue');
const About = () => import('./pages/About.vue');
const Dashboard = () => import('./pages/Dashboard.vue');
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/dashboard', component: Dashboard }
];
// Webpack magic comments
import(/* webpackChunkName: "charts" */ './charts').then(module => {
const { renderChart } = module;
renderChart(data);
});
// Conditional loading
if (condition) {
import(/* webpackChunkName: "heavy-feature" */ './heavy-feature')
.then(module => module.init());
}
Lazy Loading Techniques
Load resources only when needed to improve initial page load time.
Image Lazy Loading
<!-- Native lazy loading -->
<img src="image.jpg"
alt="Description"
loading="lazy"
width="800"
height="600">
<!-- Responsive images with lazy loading -->
<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="Description"
loading="lazy">
// Intersection Observer for custom lazy loading
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.srcset = img.dataset.srcset || '';
img.classList.remove('lazy');
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
Component Lazy Loading
// React lazy loading
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// Vue 3 lazy loading
import { defineAsyncComponent } from 'vue';
export default {
components: {
AsyncComponent: defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
timeout: 3000
})
}
};
Lazy Loading Third-Party Scripts
// Load analytics only when needed
function loadAnalytics() {
if (window.ga) return;
const script = document.createElement('script');
script.async = true;
script.src = 'https://www.google-analytics.com/analytics.js';
document.head.appendChild(script);
}
// Load on user interaction
document.addEventListener('scroll', loadAnalytics, { once: true });
document.addEventListener('click', loadAnalytics, { once: true });
// Or delay until page is idle
if ('requestIdleCallback' in window) {
requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
setTimeout(loadAnalytics, 2000);
}
Content Visibility API
/* Defer rendering of off-screen content */
.section {
content-visibility: auto;
contain-intrinsic-size: 0 500px;
}
/* Skip rendering until needed */
.hidden-section {
content-visibility: hidden;
}
Image Optimisation
Optimise images for faster loading and better performance.
Responsive Images
<!-- Art direction with picture element -->
<picture>
<source media="(max-width: 799px)"
srcset="mobile-image.webp"
type="image/webp">
<source media="(max-width: 799px)"
srcset="mobile-image.jpg">
<source media="(min-width: 800px)"
srcset="desktop-image.webp"
type="image/webp">
<source media="(min-width: 800px)"
srcset="desktop-image.jpg">
<img src="desktop-image.jpg" alt="Description">
</picture>
<!-- Density descriptors for retina displays -->
<img srcset="image-1x.jpg 1x,
image-2x.jpg 2x,
image-3x.jpg 3x"
src="image-1x.jpg"
alt="Description">
Modern Image Formats
<!-- WebP with fallback -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>
Image Compression
// Sharp (Node.js) for server-side processing
const sharp = require('sharp');
async function optimiseImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(1200, null, {
withoutEnlargement: true,
fit: 'inside'
})
.webp({ quality: 85 })
.toFile(outputPath);
}
// Generate multiple sizes
async function generateResponsiveImages(input) {
const sizes = [400, 800, 1200, 1600];
await Promise.all(
sizes.map(width =>
sharp(input)
.resize(width)
.webp({ quality: 85 })
.toFile(`output-${width}.webp`)
)
);
}
CSS Background Images
/* Responsive background images */
.hero {
background-image: url('hero-small.jpg');
}
@media (min-width: 768px) {
.hero {
background-image: url('hero-medium.jpg');
}
}
@media (min-width: 1200px) {
.hero {
background-image: url('hero-large.jpg');
}
}
/* Modern format with fallback */
.hero {
background-image: url('hero.jpg');
background-image: image-set(
url('hero.avif') type('image/avif'),
url('hero.webp') type('image/webp'),
url('hero.jpg') type('image/jpeg')
);
}
Blur-up Technique
<!-- Low quality image placeholder -->
<div class="image-wrapper">
<img class="placeholder"
src="tiny-blurred-10x10.jpg"
alt="Description">
<img class="full-image"
data-src="full-quality.jpg"
alt="Description">
</div>
.image-wrapper {
position: relative;
overflow: hidden;
}
.placeholder {
filter: blur(20px);
transform: scale(1.1);
transition: opacity 0.3s;
}
.full-image {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s;
}
.full-image.loaded {
opacity: 1;
}
.full-image.loaded ~ .placeholder {
opacity: 0;
}
Performance Monitoring
Measure and monitor performance to identify bottlenecks.
Web Vitals
// Using web-vitals library (v4+ API)
// The get*() functions were removed in v4; FID was retired in favour of INP.
import { onCLS, onINP, onFCP, onLCP, onTTFB } from 'web-vitals';
function sendToAnalytics({ name, value, id, delta }) {
// Send to analytics endpoint
navigator.sendBeacon('/analytics', JSON.stringify({
metric: name,
value: Math.round(value),
id,
delta: Math.round(delta)
}));
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onFCP(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);
Performance Observer API
// Monitor long tasks
const longTaskObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
// Monitor resource timing
const resourceObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.duration > 1000) {
console.warn('Slow resource:', {
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
type: entry.initiatorType
});
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
// Monitor navigation timing
const navigationObserver = new PerformanceObserver(list => {
const entry = list.getEntries()[0];
console.log('Navigation metrics:', {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
domInteractive: entry.domInteractive,
domComplete: entry.domComplete,
loadComplete: entry.loadEventEnd
});
});
navigationObserver.observe({ entryTypes: ['navigation'] });
Custom Performance Marks
// Mark important events
performance.mark('component-init-start');
await initializeComponent();
performance.mark('component-init-end');
// Measure duration
performance.measure(
'component-initialization',
'component-init-start',
'component-init-end'
);
// Get measurements
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
console.log(`${measure.name}: ${measure.duration}ms`);
});
// Clear marks when done
performance.clearMarks();
performance.clearMeasures();
Performance Budget
// webpack-bundle-analyzer configuration
const BundleBudgetPlugin = require('webpack-bundle-budget-plugin');
module.exports = {
plugins: [
new BundleBudgetPlugin({
budgets: [
{
resourceType: 'script',
maximumSize: 250 * 1024 // 250 KB
},
{
resourceType: 'style',
maximumSize: 100 * 1024 // 100 KB
},
{
resourceType: 'image',
maximumSize: 500 * 1024 // 500 KB
}
]
})
]
};
flowchart TB
A[Build Process] --> B{Check Bundle Size}
B -->|Under Budget| C[Deploy]
B -->|Over Budget| D[Alert Team]
D --> E[Analyse Bundle]
E --> F[Optimise Code]
F --> A
Performance Monitoring Tools
Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:3000
http://localhost:3000/about
budgetPath: ./budget.json
uploadArtifacts: true
// budget.json
[
{
"path": "/*",
"timings": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1500
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "total",
"budget": 1000
}
]
}
]
Real User Monitoring (RUM)
// Custom RUM implementation
class RUMTracker {
constructor(endpoint) {
this.endpoint = endpoint;
this.metrics = {};
this.init();
}
init() {
// Capture navigation timing
window.addEventListener('load', () => {
const navTiming = performance.getEntriesByType('navigation')[0];
this.metrics.navigation = {
dns: navTiming.domainLookupEnd - navTiming.domainLookupStart,
tcp: navTiming.connectEnd - navTiming.connectStart,
ttfb: navTiming.responseStart - navTiming.requestStart,
download: navTiming.responseEnd - navTiming.responseStart,
domInteractive: navTiming.domInteractive,
domComplete: navTiming.domComplete
};
this.send();
});
// Capture web vitals
this.observeWebVitals();
// Capture errors
this.observeErrors();
}
observeWebVitals() {
import('web-vitals').then(({ onCLS, onINP, onLCP }) => {
onCLS(metric => this.metrics.cls = metric.value);
onINP(metric => this.metrics.inp = metric.value);
onLCP(metric => this.metrics.lcp = metric.value);
});
}
observeErrors() {
window.addEventListener('error', event => {
this.metrics.errors = this.metrics.errors || [];
this.metrics.errors.push({
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno
});
});
}
send() {
const data = {
...this.metrics,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
navigator.sendBeacon(this.endpoint, JSON.stringify(data));
}
}
// Initialize tracker
const rum = new RUMTracker('/api/metrics');
Common Bottlenecks and Solutions
Render-Blocking Resources
<!-- Problem: Blocking CSS -->
<link rel="stylesheet" href="styles.css">
<!-- Solution: Load non-critical 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>
<!-- Problem: Blocking JavaScript -->
<script src="app.js"></script>
<!-- Solution: Defer or async -->
<script src="app.js" defer></script>
Layout Thrashing
// Problem: Forced synchronous layout
function badResize() {
elements.forEach(el => {
const width = el.offsetWidth; // Read
el.style.width = width * 2 + 'px'; // Write
const height = el.offsetHeight; // Read - forces layout
el.style.height = height * 2 + 'px'; // Write
});
}
// Solution: Batch reads and writes
function goodResize() {
// Batch all reads
const dimensions = elements.map(el => ({
width: el.offsetWidth,
height: el.offsetHeight
}));
// Batch all writes
elements.forEach((el, i) => {
el.style.width = dimensions[i].width * 2 + 'px';
el.style.height = dimensions[i].height * 2 + 'px';
});
}
// Or use requestAnimationFrame
function optimisedResize() {
if (resizeScheduled) return;
resizeScheduled = true;
requestAnimationFrame(() => {
goodResize();
resizeScheduled = false;
});
}
Memory Leaks
// Problem: Event listeners not cleaned up
class Component {
constructor() {
window.addEventListener('resize', this.handleResize);
}
handleResize() {
// Handle resize
}
}
// Solution: Clean up in destructor
class Component {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
handleResize() {
// Handle resize
}
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}
// Problem: Detached DOM nodes
let cache = [];
function addToCache(element) {
cache.push(element); // Element never garbage collected
}
// Solution: Use WeakMap or WeakSet
const cache = new WeakMap();
function addToCache(element, data) {
cache.set(element, data); // Automatically garbage collected
}
Excessive Repaints
/* Problem: Animating layout properties */
.box {
transition: width 0.3s;
}
.box:hover {
width: 200px; /* Triggers layout */
}
/* Solution: Animate transform instead */
.box {
transition: transform 0.3s;
}
.box:hover {
transform: scaleX(2); /* GPU accelerated */
}
/* Use will-change for complex animations */
.animated {
will-change: transform, opacity;
}
/* Promote to own layer */
.gpu-accelerated {
transform: translateZ(0);
/* or */
will-change: transform;
}
Large DOM Trees
// Problem: Rendering large lists
function renderList(items) {
return items.map(item => `<li>${item}</li>`).join('');
}
// Solution: Virtual scrolling
class VirtualList {
constructor(container, items, rowHeight) {
this.container = container;
this.items = items;
this.rowHeight = rowHeight;
this.visibleRows = Math.ceil(container.clientHeight / rowHeight);
this.init();
}
init() {
this.container.style.height = this.items.length * this.rowHeight + 'px';
this.container.addEventListener('scroll', () => this.render());
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.rowHeight);
const endIndex = startIndex + this.visibleRows + 1;
const visibleItems = this.items.slice(startIndex, endIndex);
const offset = startIndex * this.rowHeight;
this.container.innerHTML = `
<div style="padding-top: ${offset}px">
${visibleItems.map(item => `<div style="height: ${this.rowHeight}px">${item}</div>`).join('')}
</div>
`;
}
}
JavaScript Bundle Size
// Problem: Importing entire libraries
import _ from 'lodash';
// Solution: Import only what you need
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
// Or use native alternatives
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};
// Dynamic imports for heavy dependencies
async function loadHeavyFeature() {
const module = await import(/* webpackChunkName: "heavy" */ './heavy-module');
module.init();
}
Quick Reference
Performance Checklist
| Optimisation | Impact | Effort |
|---|---|---|
| Enable compression (gzip/Brotli) | High | Low |
| Optimise images (WebP, AVIF) | High | Medium |
| Implement lazy loading | High | Low |
| Add caching headers | High | Low |
| Minify CSS/JS | Medium | Low |
| Code splitting | High | Medium |
| Use CDN | High | Low |
| Defer non-critical JS | High | Low |
| Inline critical CSS | Medium | Medium |
| Remove unused CSS | Medium | Medium |
| Optimise web fonts | Medium | Low |
| Enable HTTP/2 | Medium | Low |
| Implement service worker | High | High |
| Reduce third-party scripts | High | Medium |
Core Web Vitals Targets
INP (Interaction to Next Paint) replaced FID as a Core Web Vital on 12 March 2024.
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5s - 4s | > 4s |
| INP (Interaction to Next Paint) | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
HTTP Cache Headers
# Static assets (versioned)
Cache-Control: public, max-age=31536000, immutable
# Static assets (unversioned)
Cache-Control: public, max-age=86400, must-revalidate
# HTML pages
Cache-Control: public, max-age=0, must-revalidate
# API responses (cacheable)
Cache-Control: public, max-age=300, must-revalidate
# API responses (private)
Cache-Control: private, max-age=300, must-revalidate
# Never cache
Cache-Control: no-store, no-cache, must-revalidate
Resource Hints Priority
<!-- Highest priority -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- High priority -->
<link rel="preload" href="critical.css" as="style">
<!-- Medium priority -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- Low priority -->
<link rel="prefetch" href="next-page.html">
<!-- Prerender: use the Speculation Rules API -->
<!-- (<link rel="prerender"> is deprecated and Chrome-only) -->
<script type="speculationrules">
{ "prerender": [{ "urls": ["likely-next.html"] }] }
</script>
Common Issues and Solutions
| Issue | Symptoms | Solution |
|---|---|---|
| Slow initial load | High FCP/LCP times | Optimise critical rendering path, reduce bundle size |
| Layout shifts | High CLS score | Reserve space for dynamic content, use aspect ratios |
| Janky scrolling | Low frame rate | Use CSS transforms, avoid layout thrashing |
| Memory leaks | Increasing memory usage | Clean up event listeners, use WeakMap/WeakSet |
| Large bundle size | Slow download times | Code splitting, tree shaking, dynamic imports |
| Slow API responses | High TTFB | Implement server-side caching, use CDN |
| Render blocking | Delayed FCP | Defer/async scripts, load critical CSS inline |
| Too many requests | Network waterfall | Bundle resources, use HTTP/2, implement caching |
| Large images | Slow LCP | Optimise formats (WebP/AVIF), lazy load, responsive images |
| Third-party scripts | Slow TTI | Load asynchronously, delay until interaction |
Debugging Performance Issues
// Measure function performance
console.time('myFunction');
myFunction();
console.timeEnd('myFunction');
// Profile with Performance API
performance.mark('start');
await heavyOperation();
performance.mark('end');
performance.measure('heavyOperation', 'start', 'end');
const measure = performance.getEntriesByName('heavyOperation')[0];
console.log(`Operation took ${measure.duration}ms`);
// Monitor FPS
let lastTime = performance.now();
let frames = 0;
function measureFPS() {
const currentTime = performance.now();
frames++;
if (currentTime >= lastTime + 1000) {
const fps = Math.round((frames * 1000) / (currentTime - lastTime));
console.log(`FPS: ${fps}`);
frames = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}
measureFPS();
// Detect long tasks
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['longtask'] });
}
Testing Tools
# Lighthouse audit
npx lighthouse https://example.com --view
# WebPageTest
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&k=API_KEY"
# Chrome DevTools Coverage
# Open DevTools > More Tools > Coverage > Record
# Bundle analysis
npx webpack-bundle-analyzer dist/stats.json