Core Web Vitals, Lighthouse, image and font optimization, caching strategies, code splitting, performance budgets, and common anti-patterns.
Web Performance
A fast website is not optional - it affects user experience, conversion rates, SEO rankings, and accessibility. This guide covers the metrics, techniques, and strategies that matter most.
Core Web Vitals
Google's Core Web Vitals are the three metrics that define a good user experience:
LCP - Largest Contentful Paint
Measures loading performance: how long until the largest visible element (hero image, heading block, video poster) is rendered.
Common LCP elements: hero images, <h1> text blocks, video poster frames, large SVGs.
What hurts LCP:
- Slow server response (high TTFB)
- Render-blocking CSS or JavaScript
- Large, unoptimized images
- Lazy-loading the LCP element (do not lazy-load the hero)
What helps LCP:
- Preload the LCP image:
<link rel="preload" as="image" href="hero.webp"> - Use a CDN to reduce server latency
- Inline critical CSS
- Set
fetchpriority="high"on the LCP image
INP - Interaction to Next Paint
Measures interactivity: how long between a user action (click, tap, keypress) and the next visual update.
What hurts INP:
- Long JavaScript tasks (> 50ms) blocking the main thread
- Heavy event handlers
- Layout thrashing (reading then writing DOM in a loop)
- Third-party scripts competing for the main thread
What helps INP:
- Break long tasks with
requestIdleCallbackorscheduler.yield() - Debounce input handlers
- Use
requestAnimationFramefor visual updates - Move heavy work to Web Workers
CLS - Cumulative Layout Shift
Measures visual stability: how much visible content moves unexpectedly during loading.
What causes CLS:
- Images without dimensions (
width/heightattributes or CSSaspect-ratio) - Ads, embeds, or iframes that resize after load
- Web fonts causing text reflow (FOIT/FOUT)
- Dynamically injected content above the fold
What prevents CLS:
- Always set
widthandheighton images and videos - Use
aspect-ratioin CSS for responsive containers - Reserve space for ads and embeds
- Use
font-display: optionalto avoid font-swap layout shifts
Measuring performance
Lab tools (synthetic)
Field data (real users)
Lab vs field:
Always look at both. Lab catches regressions during development; field confirms the impact on real users.
Images
Images are typically the heaviest assets on a page. Optimizing them has the biggest impact on LCP and total page weight.
Modern formats
The <picture> element
Serve the best format the browser supports:
Responsive images
Serve different sizes for different viewports:
Image best practices
- Always set dimensions -
widthandheightattributes prevent CLS - Lazy-load below-the-fold images -
loading="lazy"on<img> - Do NOT lazy-load the LCP image - it must load immediately
- Use
fetchpriority="high"on the LCP image - Use
decoding="async"on non-critical images - Compress images - aim for 80-85% quality, rarely noticeable below that
- Use an image CDN (Cloudinary, imgix, Vercel Image Optimization) for automatic resizing and format conversion
Fonts
Web fonts can cause both layout shifts (CLS) and render delays (LCP).
font-display values
Recommendation: Use font-display: optional for body text (avoids all layout shift). Use swap only if the brand font is critical to the design.
Font loading strategies
- Self-host fonts instead of loading from Google Fonts - avoids a DNS lookup, connection, and the Google Fonts CSS file
- Use WOFF2 - best compression, supported by all modern browsers
- Subset fonts - include only the character sets you need (latin, latin-ext)
- Limit font weights - every weight is a separate file; use 2-3 weights maximum
Preventing layout shift from fonts
Using font-display: optional combined with preloading gives you the web font on repeat visits (cached) and the system font on first visit (no layout shift).
JavaScript
Code splitting
Load only the JavaScript needed for the current page:
Frameworks handle this automatically:
Script loading
Tree shaking
Bundlers (webpack, Rollup, esbuild) remove unused exports from your bundles. For it to work:
- Use ES modules (
import/export), not CommonJS (require) - Avoid side effects in modules
- Mark packages as side-effect-free in
package.json:"sideEffects": false
Avoiding long tasks
A long task is any JavaScript execution that takes longer than 50ms, blocking the main thread and hurting INP.
- Break large loops with
requestIdleCallbackorscheduler.yield() - Defer non-critical work to after the page loads
- Use Web Workers for CPU-intensive computation (parsing, sorting, image processing)
- Profile with Chrome DevTools Performance panel - look for long yellow bars
CSS
Critical CSS
Inline the CSS needed to render above-the-fold content in the <head>:
Tools like critical (npm) extract critical CSS automatically.
Reducing CSS
- Remove unused CSS - tools: PurgeCSS, Tailwind's built-in purge, Coverage tab in Chrome DevTools
- Avoid
@importin CSS files - it creates sequential requests; use bundler imports instead - Use
content-visibility: autoon below-the-fold sections - skips rendering until the element is near the viewport
Avoiding render-blocking CSS
All <link rel="stylesheet"> tags block rendering by default. Mitigation:
- Inline critical CSS (above)
- Use
mediaattributes for conditional styles:<link rel="stylesheet" href="print.css" media="print"> - Load non-critical CSS asynchronously with the preload pattern
Caching strategy
Caching is the single most effective performance optimization. A cached response has zero latency.
HTTP cache headers
Cache-Control
The primary header for controlling caching:
Cache busting with hashed filenames
Include a content hash in filenames so the URL changes when the content changes:
These files can be cached forever (immutable). When you deploy a new version, the HTML references a new filename, bypassing the cache.
CDN caching
CDNs cache content at edge locations close to users. Benefits:
- Lower latency - content served from a nearby server
- Reduced origin load - the origin handles fewer requests
- DDoS protection - CDN absorbs traffic spikes
Set appropriate Cache-Control headers so the CDN knows what to cache and for how long.
stale-while-revalidate
A powerful pattern for balancing freshness and speed:
- For the first 60 seconds: serve from cache (fresh)
- After 60 seconds but within 3600 seconds: serve stale response immediately, fetch fresh in the background
- After 3600 seconds: fetch fresh (normal cache miss)
Users always get an instant response; freshness is maintained in the background.
Server and network
HTTP/2 and HTTP/3
With HTTP/2+, techniques like domain sharding and CSS sprites are no longer needed (they can actually hurt). Focus on reducing the total number of bytes instead.
Compression
Most servers and CDNs support both. Brotli is the default choice for static assets.
Resource hints
Tell the browser what to fetch ahead of time:
Performance budgets
A performance budget sets hard limits on metrics. If a build exceeds the budget, it fails.
What to budget
Enforcing budgets
- webpack:
performance.maxAssetSizeandperformance.maxEntrypointSizein config - Lighthouse CI: Set assertions in
lighthouserc.json - bundlesize: CLI tool that fails CI if bundles exceed limits
- Import Cost (VS Code extension): shows the size of imported packages inline
Anti-patterns
Common mistakes that tank performance:
Platform-specific resources
These guides cover performance in the context of specific platforms used in this documentation:
AEM:
- Dispatcher Configuration - caching at the Dispatcher layer
- Performance - AEM-specific performance optimization
Strapi:
- Performance and Caching - API response caching, database optimization
JavaScript:
- Performance Basics - client-side JavaScript performance patterns
- MDN MCP Server - query live browser support for performance APIs (
PerformanceObserver,fetchpriority, etc.) directly in your editor