🚀
Advanced
12 min read

Web Performance

Performance optimization, metrics, and best practices

Web Performance Interview Questions

Master web performance optimization concepts. This guide covers performance metrics, optimization techniques, lazy loading, code splitting, and best practices.

1. Core Web Vitals

Google's Core Web Vitals are essential metrics for measuring user experience.

Three Core Web Vitals:
  • LCP (Largest Contentful Paint): Time to render largest content element (Goal: < 2.5s)
  • FID (First Input Delay): Time from user interaction to browser response (Goal: < 100ms)
  • CLS (Cumulative Layout Shift): Visual stability, unexpected layout shifts (Goal: < 0.1)

Measuring with JavaScript:

// Performance Observer API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP:', entry.startTime);
    console.log('Element:', entry.element);
  }
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });

// First Input Delay
const fidObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    const fid = entry.processingStart - entry.startTime;
    console.log('FID:', fid);
  }
});
fidObserver.observe({ entryTypes: ['first-input'] });

// Layout Shift
let clsScore = 0;
const clsObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsScore += entry.value;
    }
  }
  console.log('CLS:', clsScore);
});
clsObserver.observe({ entryTypes: ['layout-shift'] });

2. Image Optimization

<!-- Modern formats with fallback -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description" loading="lazy">
</picture>

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

<!-- Lazy loading -->
<img src="image.jpg" loading="lazy" alt="Lazy loaded">

<!-- Priority hints -->
<img src="hero.jpg" fetchpriority="high" alt="Hero image">
<img src="footer.jpg" fetchpriority="low" alt="Footer image">

JavaScript Lazy Loading:

// Intersection Observer for lazy loading
const images = document.querySelectorAll('img[data-src]');

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);
    }
  });
}, {
  rootMargin: '50px' // Start loading 50px before viewport
});

images.forEach(img => imageObserver.observe(img));

3. Code Splitting and Lazy Loading

React Code Splitting:

import { lazy, Suspense } from 'react';

// Dynamic import
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

// Route-based splitting
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

// Preload on hover
function Link({ to, children }) {
  const preload = () => {
    if (to === '/about') {
      import('./pages/About');
    }
  };

  return (
    <a href={to} onMouseEnter={preload}>
      {children}
    </a>
  );
}

Webpack Magic Comments:

// Named chunk
import(/* webpackChunkName: "my-chunk" */ './module');

// Prefetch (load during idle time)
import(/* webpackPrefetch: true */ './module');

// Preload (load in parallel with parent)
import(/* webpackPreload: true */ './module');

4. Critical CSS and Resource Loading

<head>
  <!-- Critical CSS inline -->
  <style>
    /* Above-the-fold styles */
    body { margin: 0; font-family: sans-serif; }
    .header { background: #333; color: white; }
  </style>
  
  <!-- Preload critical resources -->
  <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="hero.jpg" as="image">
  <link rel="preload" href="main.js" as="script">
  
  <!-- DNS prefetch for external resources -->
  <link rel="dns-prefetch" href="https://api.example.com">
  
  <!-- Preconnect to establish connection early -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  
  <!-- Non-critical CSS with media query -->
  <link rel="stylesheet" href="print.css" media="print" onload="this.media='all'">
  
  <!-- Defer non-critical CSS -->
  <link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>

5. JavaScript Performance Optimization

// Debouncing expensive operations
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

const handleSearch = debounce((query) => {
  // Expensive search operation
}, 300);

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

const handleScroll = throttle(() => {
  // Handle scroll
}, 200);

// requestAnimationFrame for smooth animations
let scrollPos = 0;
function smoothScroll() {
  scrollPos += 1;
  window.scrollTo(0, scrollPos);
  
  if (scrollPos < 1000) {
    requestAnimationFrame(smoothScroll);
  }
}

// Web Workers for heavy computation
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

// Virtual scrolling for large lists
function VirtualList({ items, itemHeight }) {
  const [scrollTop, setScrollTop] = useState(0);
  const visibleStart = Math.floor(scrollTop / itemHeight);
  const visibleEnd = visibleStart + Math.ceil(window.innerHeight / itemHeight);
  const visibleItems = items.slice(visibleStart, visibleEnd);
  
  return (
    <div onScroll={(e) => setScrollTop(e.target.scrollTop)}>
      <div style={{ height: items.length * itemHeight }}>
        <div style={{ transform: `translateY(${visibleStart * itemHeight}px)` }}>
          {visibleItems.map(item => <Item key={item.id} {...item} />)}
        </div>
      </div>
    </div>
  );
}

6. Bundle Size Optimization

// Tree shaking - import only what you need
// ❌ Bad
import _ from 'lodash';
const result = _.debounce(func, 300);

// ✅ Good
import debounce from 'lodash/debounce';
const result = debounce(func, 300);

// Dynamic imports
if (condition) {
  import('./heavy-module').then(module => {
    module.doSomething();
  });
}

// Remove console.logs in production
if (process.env.NODE_ENV !== 'production') {
  console.log('Debug info');
}

// Analyze bundle size
// npm install --save-dev webpack-bundle-analyzer
// Add to webpack config:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

7. Caching Strategies

// HTTP Caching Headers
// In server configuration:
Cache-Control: public, max-age=31536000, immutable // Static assets
Cache-Control: no-cache // HTML (revalidate each time)
Cache-Control: private, max-age=3600 // User-specific data

// Service Worker caching
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js',
        '/images/logo.png'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Cache hit - return response
      if (response) {
        return response;
      }
      // Fetch from network
      return fetch(event.request);
    })
  );
});

// LocalStorage caching with expiry
function setWithExpiry(key, value, ttl) {
  const item = {
    value: value,
    expiry: Date.now() + ttl
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;
  
  const item = JSON.parse(itemStr);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

// Memoization
const memoize = (fn) => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

const expensiveOperation = memoize((n) => {
  // Complex calculation
  return n * 2;
});

8. Network Performance

// HTTP/2 Server Push
// In server configuration:
Link: ; rel=preload; as=style
Link: ; rel=preload; as=script

// Compression (gzip/brotli)
// Enable in server configuration
Content-Encoding: br // Brotli (better compression)
Content-Encoding: gzip // Gzip (broader support)

// CDN and edge caching
// Use CDN for static assets
https://cdn.example.com/assets/image.jpg

// Resource hints
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="preconnect" href="https://api.example.com">
<link rel="prefetch" href="/next-page.html">
<link rel="prerender" href="/next-page.html">

// Optimize API calls
// Batch requests
const [users, posts, comments] = await Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
]);

// Use GraphQL to fetch only needed data
query {
  user(id: 1) {
    name
    email
    // Only fetch what you need
  }
}

9. Performance Monitoring

// Performance API
const perfData = performance.getEntriesByType('navigation')[0];
console.log('DNS lookup:', perfData.domainLookupEnd - perfData.domainLookupStart);
console.log('TCP connection:', perfData.connectEnd - perfData.connectStart);
console.log('Request time:', perfData.responseStart - perfData.requestStart);
console.log('Response time:', perfData.responseEnd - perfData.responseStart);
console.log('DOM processing:', perfData.domComplete - perfData.domLoading);
console.log('Load complete:', perfData.loadEventEnd - perfData.navigationStart);

// Custom performance marks
performance.mark('start-fetch');
await fetch('/api/data');
performance.mark('end-fetch');
performance.measure('fetch-duration', 'start-fetch', 'end-fetch');

const measures = performance.getEntriesByName('fetch-duration');
console.log('Fetch took:', measures[0].duration, 'ms');

// Long Tasks API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Long task detected:', entry.duration);
  }
});
observer.observe({ entryTypes: ['longtask'] });

// User Timing
performance.mark('component-render-start');
// ... component renders
performance.mark('component-render-end');
performance.measure(
  'component-render',
  'component-render-start',
  'component-render-end'
);
Key Interview Takeaways:
  • Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
  • Use modern image formats (WebP, AVIF) with fallbacks
  • Implement lazy loading for images and components
  • Code splitting for smaller initial bundles
  • Critical CSS inline, defer non-critical
  • Debounce/throttle expensive operations
  • Use CDN and enable compression (Brotli/gzip)
  • Implement effective caching strategies
  • Monitor performance with Performance API
  • Tree shake unused code, analyze bundle size