Design.dev design.dev

Web Performance Optimization Guide

Master web performance optimization with Core Web Vitals, loading strategies, and proven techniques. Learn to measure, monitor, and improve your site's speed for better user experience and SEO.

Core Web Vitals

Google's essential metrics for measuring user experience quality.

Largest Contentful Paint (LCP)

Loading performance - How fast the main content loads

1.8s
Good ≤ 2.5s Needs Improvement ≤ 4s Poor > 4s
// Measure LCP with PerformanceObserver
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });

// Common LCP elements:
// - Images (img, svg)
// - Video poster images
// - Background images (via url())
// - Block-level text elements

Cumulative Layout Shift (CLS)

Visual stability - How much the page shifts during loading

0.05
Good ≤ 0.1 Needs Improvement ≤ 0.25 Poor > 0.25
/* Prevent layout shifts */
img, video {
  /* Always include dimensions */
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* Reserve space for dynamic content */
.ad-container {
  min-height: 250px;
}

/* Use CSS containment */
.dynamic-content {
  contain: layout style paint;
}

/* Avoid inserting content above existing content */
/* Use transform instead of top/left for animations */

Interaction to Next Paint (INP)

Responsiveness - How quickly the page responds to interactions

Replacing FID
85ms
Good ≤ 200ms Needs Improvement ≤ 500ms Poor > 500ms
// Optimize JavaScript execution
// Break up long tasks
function processLargeArray(array) {
  const chunkSize = 100;
  let index = 0;
  
  function processChunk() {
    const chunk = array.slice(index, index + chunkSize);
    // Process chunk
    chunk.forEach(item => processItem(item));
    
    index += chunkSize;
    if (index < array.length) {
      // Yield to main thread
      requestIdleCallback(processChunk);
    }
  }
  
  processChunk();
}

// Use Web Workers for heavy computation
const worker = new Worker('heavy-computation.js');
worker.postMessage({ cmd: 'process', data: largeDataSet });

Why Core Web Vitals Matter: They're ranking factors for Google Search and directly impact user experience. Poor scores lead to higher bounce rates and lower conversions.

Loading Performance

Optimize how and when resources are loaded to improve perceived performance.

Resource Hints

<!-- DNS Prefetch - Resolve DNS early -->
<link rel="dns-prefetch" href="//api.example.com">

<!-- Preconnect - DNS + TCP + TLS -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Prefetch - Download for future navigation -->
<link rel="prefetch" href="/next-page.html">

<!-- Preload - High priority resource for current page -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app.js" as="script">

<!-- Modulepreload - Preload ES modules -->
<link rel="modulepreload" href="/js/module.js">

Script Loading Strategies

🚫 Render Blocking
<script src="app.js"></script>

Blocks HTML parsing and rendering

✅ Async
<script async src="app.js"></script>

Downloads in parallel, executes immediately

✅ Defer
<script defer src="app.js"></script>

Downloads in parallel, executes after DOM

Resource Loading Waterfall

HTML
Parse
CSS (head)
Download & Parse
JS (defer)
Download → Execute
Images
Lazy Load
Fonts
Preload

Lazy Loading

<!-- Native lazy loading for images -->
<img src="hero.jpg" alt="Hero image" loading="eager"> <!-- Above the fold -->
<img src="product.jpg" alt="Product" loading="lazy"> <!-- Below the fold -->

<!-- Native lazy loading for iframes -->
<iframe src="video.html" loading="lazy"></iframe>

<!-- Intersection Observer for custom lazy loading -->
<script>
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.add('loaded');
      imageObserver.unobserve(img);
    }
  });
}, {
  rootMargin: '50px 0px' // Start loading 50px before visible
});

lazyImages.forEach(img => imageObserver.observe(img));
</script>

Lazy Loading Visualization

Loaded
Loaded
Loading...
Not loaded

Priority Hints

<!-- Fetch Priority API (New) -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<img src="footer-logo.jpg" fetchpriority="low" alt="Logo">

<!-- Adjust script priority -->
<script src="critical.js" fetchpriority="high"></script>
<script src="analytics.js" fetchpriority="low" async></script>

<!-- Link priority -->
<link rel="stylesheet" href="critical.css" fetchpriority="high">

Runtime Performance

Optimize JavaScript execution and rendering for smooth interactions.

JavaScript Optimization

// Debounce expensive operations
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Usage
const expensiveOperation = debounce(() => {
  // Heavy computation
}, 300);

// Throttle continuous events
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Use requestAnimationFrame for visual updates
function smoothScroll(element, target, duration) {
  const start = element.scrollTop;
  const change = target - start;
  const startTime = performance.now();
  
  function animateScroll(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    element.scrollTop = start + change * easeInOutQuad(progress);
    
    if (progress < 1) {
      requestAnimationFrame(animateScroll);
    }
  }
  
  requestAnimationFrame(animateScroll);
}

Virtual Scrolling

// Simple virtual scrolling implementation
class VirtualScroller {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
    this.totalHeight = items.length * itemHeight;
    
    this.init();
  }
  
  init() {
    // Create viewport
    this.viewport = document.createElement('div');
    this.viewport.style.height = `${this.totalHeight}px`;
    
    // Create content container
    this.content = document.createElement('div');
    this.content.style.transform = 'translateY(0)';
    
    this.container.appendChild(this.viewport);
    this.container.appendChild(this.content);
    
    this.container.addEventListener('scroll', () => this.render());
    this.render();
  }
  
  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = startIndex + this.visibleItems + 1;
    
    // Clear content
    this.content.innerHTML = '';
    
    // Render visible items
    for (let i = startIndex; i < endIndex && i < this.items.length; i++) {
      const item = this.createItem(this.items[i], i);
      this.content.appendChild(item);
    }
    
    // Position content
    this.content.style.transform = `translateY(${startIndex * this.itemHeight}px)`;
  }
  
  createItem(data, index) {
    const div = document.createElement('div');
    div.style.height = `${this.itemHeight}px`;
    div.textContent = data;
    return div;
  }
}

Memory Management

// Avoid memory leaks
class ComponentWithCleanup {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
    this.observers = new Set();
  }
  
  mount() {
    // Add event listeners
    document.addEventListener('click', this.handleClick);
    window.addEventListener('scroll', this.handleScroll);
    
    // Create observers
    this.resizeObserver = new ResizeObserver(entries => {
      // Handle resize
    });
    this.resizeObserver.observe(this.element);
  }
  
  unmount() {
    // Remove event listeners
    document.removeEventListener('click', this.handleClick);
    window.removeEventListener('scroll', this.handleScroll);
    
    // Disconnect observers
    this.resizeObserver.disconnect();
    
    // Clear references
    this.observers.clear();
    this.data = null;
  }
}

// Use WeakMap for metadata
const metadata = new WeakMap();
function setMetadata(obj, data) {
  metadata.set(obj, data);
}

// Object pooling for frequent allocations
class ObjectPool {
  constructor(createFn, resetFn, maxSize = 100) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
    this.maxSize = maxSize;
  }
  
  acquire() {
    if (this.pool.length > 0) {
      return this.pool.pop();
    }
    return this.createFn();
  }
  
  release(obj) {
    if (this.pool.length < this.maxSize) {
      this.resetFn(obj);
      this.pool.push(obj);
    }
  }
}

Asset Optimization

Reduce file sizes and optimize delivery of images, fonts, and other assets.

Image Optimization

<!-- Modern image formats with fallbacks -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description" 
       loading="lazy"
       decoding="async"
       width="800" 
       height="600">
</picture>

<!-- Responsive images -->
<img srcset="small.jpg 480w,
             medium.jpg 800w,
             large.jpg 1200w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 50vw,
            800px"
     src="medium.jpg"
     alt="Responsive image">

<!-- Art direction with picture -->
<picture>
  <source media="(min-width: 1200px)" srcset="desktop.jpg">
  <source media="(min-width: 768px)" srcset="tablet.jpg">
  <img src="mobile.jpg" alt="Art directed image">
</picture>
Format Best For Compression Browser Support
AVIF Photos, complex images Best Chrome, Firefox
WebP Photos, graphics Excellent All modern browsers
JPEG Photos Good Universal
PNG Graphics, transparency Fair Universal
SVG Icons, logos, simple graphics Excellent Universal

Font Optimization

/* Preload critical fonts */
/* In HTML:  */

/* Font display strategies */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2'),
       url('font.woff') format('woff');
  font-display: swap; /* Show fallback immediately */
  /* Other options:
     - block: Hide text up to 3s (not recommended)
     - fallback: Hide briefly, then fallback
     - optional: Use if available immediately
  */
}

/* Variable fonts for multiple weights */
@font-face {
  font-family: 'VariableFont';
  src: url('variable.woff2') format('woff2-variations');
  font-weight: 100 900; /* Full weight range */
}

/* Subset fonts for specific characters */
@font-face {
  font-family: 'CustomFont';
  src: url('font-subset.woff2') format('woff2');
  unicode-range: U+0020-007E; /* Basic Latin */
}
💡 Font Loading Best Practices
  • Use woff2 format (30% smaller than woff)
  • Subset fonts to needed characters
  • Preload critical fonts
  • Use font-display: swap for body text
  • Consider system font stack for performance

CSS Optimization

<!-- Critical CSS inline -->
<style>
  /* Above-the-fold styles */
  body { margin: 0; font-family: system-ui; }
  .hero { height: 100vh; background: #000; }
</style>

<!-- 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>

<!-- Or use media attribute trick -->
<link rel="stylesheet" href="print.css" media="print" onload="this.media='all'">

JavaScript Bundle Optimization

// Code splitting with dynamic imports
// Instead of importing everything
import { heavyLibrary } from './heavy-library';

// Load on demand
button.addEventListener('click', async () => {
  const { heavyLibrary } = await import('./heavy-library');
  heavyLibrary.doSomething();
});

// Route-based code splitting (React example)
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));

// Tree shaking - use specific imports
// Bad
import _ from 'lodash';
_.debounce();

// Good
import debounce from 'lodash/debounce';
debounce();

Critical Rendering Path

Optimize the sequence of steps browsers take to render content.

1
Parse HTML
Build DOM Tree from HTML markup
2
Parse CSS
Build CSSOM Tree from stylesheets
3
Render Tree
Combine DOM and CSSOM
4
Layout
Calculate size and position
5
Paint
Fill pixels for each element
6
Composite
Combine layers and draw to screen

Optimizing the Critical Path

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  
  <!-- 1. Preconnect to required origins -->
  <link rel="preconnect" href="https://fonts.gstatic.com">
  
  <!-- 2. Inline critical CSS -->
  <style>
    /* Critical above-the-fold styles */
    body { margin: 0; font-family: -apple-system, system-ui, sans-serif; }
    .hero { min-height: 100vh; display: flex; align-items: center; }
  </style>
  
  <!-- 3. Preload critical resources -->
  <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
  <link rel="preload" href="/js/app.js" as="script">
  
  <!-- 4. Async load non-critical CSS -->
  <link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
  <!-- 5. Inline critical content -->
  <div class="hero">
    <h1>Fast Loading Page</h1>
  </div>
  
  <!-- 6. Defer non-critical scripts -->
  <script defer src="/js/app.js"></script>
  <script defer src="/js/analytics.js"></script>
</body>
</html>

Render Blocking Resources

❌ Render Blocking
<link rel="stylesheet" href="styles.css">
<script src="app.js"></script>

Blocks first paint

✅ Non-blocking
<style>/* Critical CSS */</style>
<script defer src="app.js"></script>

Allows early paint

Caching Strategies

Implement effective caching to reduce server requests and improve load times.

HTTP Cache Headers

# Immutable assets (hashed filenames)
Cache-Control: public, max-age=31536000, immutable

# Static assets that might change
Cache-Control: public, max-age=86400, stale-while-revalidate=604800

# HTML documents
Cache-Control: no-cache, must-revalidate

# API responses
Cache-Control: private, max-age=300

# No caching
Cache-Control: no-store

Service Worker Caching

// sw.js - Service Worker with caching strategies
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html'
];

// Install - cache assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// Fetch strategies
self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);
  
  // Network First (API calls)
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
  }
  // Cache First (static assets)
  else if (request.destination === 'image' || 
           request.destination === 'script' ||
           request.destination === 'style') {
    event.respondWith(cacheFirst(request));
  }
  // Stale While Revalidate (HTML)
  else {
    event.respondWith(staleWhileRevalidate(request));
  }
});

// Cache First Strategy
async function cacheFirst(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);
  
  if (cached) {
    return cached;
  }
  
  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
}

// Network First Strategy
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    return cached || new Response('Offline', { status: 503 });
  }
}

// Stale While Revalidate
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cachedResponse = await cache.match(request);
  
  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });
  
  return cachedResponse || fetchPromise;
}

Browser Storage

// localStorage for small data (sync, 5-10MB limit)
localStorage.setItem('user-preferences', JSON.stringify(prefs));

// sessionStorage for temporary data
sessionStorage.setItem('form-data', JSON.stringify(formData));

// IndexedDB for large data (async, GB+ limit)
const dbPromise = indexedDB.open('AppDB', 1);
dbPromise.onsuccess = event => {
  const db = event.target.result;
  const transaction = db.transaction(['data'], 'readwrite');
  const store = transaction.objectStore('data');
  store.put({ id: 1, data: largeDataObject });
};

// Cache API for request/response pairs
caches.open('api-cache').then(cache => {
  cache.put(request, response);
});

Performance Tools

Tools and APIs for measuring and monitoring performance.

Performance API

// Navigation Timing
const navTiming = performance.getEntriesByType('navigation')[0];
console.log('DOM Content Loaded:', navTiming.domContentLoadedEventEnd - navTiming.domContentLoadedEventStart);
console.log('Page Load Time:', navTiming.loadEventEnd - navTiming.fetchStart);

// Resource Timing
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
  console.log(`${resource.name}: ${resource.duration}ms`);
});

// User Timing API
performance.mark('myFunction-start');
myExpensiveFunction();
performance.mark('myFunction-end');

performance.measure('myFunction', 'myFunction-start', 'myFunction-end');
const measure = performance.getEntriesByName('myFunction')[0];
console.log(`myFunction took ${measure.duration}ms`);

// Performance Observer
const observer = new PerformanceObserver(list => {
  list.getEntries().forEach(entry => {
    console.log(`${entry.name}: ${entry.startTime}`);
  });
});

observer.observe({ 
  entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] 
});

Real User Monitoring (RUM)

// Collect Web Vitals
import { getCLS, getFID, getLCP, getTTFB, getINP } from 'web-vitals';

function sendToAnalytics(metric) {
  // Send to your analytics endpoint
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    url: window.location.href,
    timestamp: Date.now()
  });
  
  // Use sendBeacon for reliability
  navigator.sendBeacon('/analytics', body);
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
getINP(sendToAnalytics);

Chrome DevTools Performance

🛠️ DevTools Performance Tips
  • Network Tab: Check waterfall, size, and timing
  • Performance Tab: Record and analyze runtime performance
  • Coverage Tab: Find unused CSS and JavaScript
  • Lighthouse: Automated performance audits
  • Rendering Tab: Paint flashing, layer borders, FPS meter

Performance Budget

// performance-budget.json
{
  "timings": {
    "firstContentfulPaint": 2000,
    "largestContentfulPaint": 2500,
    "timeToInteractive": 3500,
    "totalBlockingTime": 300
  },
  "sizes": {
    "bundle": {
      "javascript": 300000,
      "css": 60000,
      "images": 500000,
      "fonts": 100000,
      "total": 1000000
    }
  },
  "requests": {
    "total": 50,
    "thirdParty": 10
  }
}

Best Practices

Essential performance optimization practices for modern web development.

Quick Wins Checklist

✅ Enable Compression

Use Gzip or Brotli compression

Content-Encoding: br

✅ Optimize Images

Use modern formats and proper sizing

<img loading="lazy" decoding="async">

✅ Minify Resources

Remove whitespace and comments

terser, cssnano, htmlmin

✅ Enable HTTP/2

Multiplexing and server push

Link: </style.css>; rel=preload

✅ Use CDN

Serve assets from edge locations

https://cdn.example.com/

✅ Remove Unused Code

Tree shake and purge CSS

PurgeCSS, Webpack tree shaking

Mobile Performance

// Adaptive loading based on connection
if ('connection' in navigator) {
  const connection = navigator.connection;
  
  // Check effective connection type
  if (connection.effectiveType === '4g') {
    // Load high quality images
    loadHighResImages();
  } else if (connection.effectiveType === '3g') {
    // Load standard quality
    loadStandardImages();
  } else {
    // Load low quality or placeholders
    loadLowQualityImages();
  }
  
  // Monitor connection changes
  connection.addEventListener('change', updateLoadingStrategy);
}

// Reduce JavaScript for low-end devices
if (navigator.hardwareConcurrency <= 4 ||
    navigator.deviceMemory <= 4) {
  // Load lighter experience
  import('./lite-version.js');
} else {
  // Load full experience
  import('./full-version.js');
}

Third-Party Script Management

<!-- Load third-party scripts efficiently -->

<!-- 1. Use async for independent scripts -->
<script async src="https://www.google-analytics.com/analytics.js"></script>

<!-- 2. Lazy load non-critical third-party content -->
<script>
// Load after user interaction
let hasInteracted = false;
['click', 'scroll', 'touchstart'].forEach(event => {
  window.addEventListener(event, () => {
    if (!hasInteracted) {
      hasInteracted = true;
      loadThirdPartyScripts();
    }
  }, { once: true, passive: true });
});

function loadThirdPartyScripts() {
  // Load chat widget
  const script = document.createElement('script');
  script.src = 'https://chat.example.com/widget.js';
  document.body.appendChild(script);
}
</script>

<!-- 3. Use facade for embeds -->
<div class="youtube-facade" data-video-id="VIDEO_ID">
  <img src="thumbnail.jpg" alt="Video thumbnail" loading="lazy">
  <button>Play</button>
</div>

Progressive Enhancement

// Feature detection and progressive enhancement
// Base experience works without JavaScript
// Enhance for capable browsers

// Check for API support
if ('IntersectionObserver' in window) {
  // Use intersection observer for lazy loading
  lazyLoadWithIntersectionObserver();
} else {
  // Fallback to scroll events
  lazyLoadWithScroll();
}

// Progressive image loading
if ('loading' in HTMLImageElement.prototype) {
  // Native lazy loading
  images.forEach(img => img.loading = 'lazy');
} else {
  // JavaScript lazy loading fallback
  loadLazyLoadingPolyfill();
}

// Network-aware loading
if ('connection' in navigator && navigator.connection.saveData) {
  // User has data saver enabled
  document.documentElement.classList.add('save-data');
  // Skip non-essential resources
}

Remember: Performance is not a one-time task but an ongoing process. Monitor, measure, and iterate. Use tools like Lighthouse CI to catch regressions in your build pipeline.