Skip to content

⚡ Performance Optimization for Interactive Demos

Status: Active Type: Guide Impact: High

📋 Overview

Performance optimization guide for interactive demonstrations and code playgrounds. This guide covers techniques, patterns, and best practices for building fast, responsive, and efficient interactive experiences that maintain excellent user experience across all devices and network conditions.

🎯 Performance Goals

Core Web Vitals Targets

Metric Target Maximum
First Contentful Paint (FCP) < 1.0s < 1.8s
Largest Contentful Paint (LCP) < 2.0s < 2.5s
First Input Delay (FID) < 50ms < 100ms
Cumulative Layout Shift (CLS) < 0.05 < 0.1
Time to Interactive (TTI) < 3.0s < 5.0s
Total Blocking Time (TBT) < 150ms < 300ms

Additional Metrics

  • Bundle Size: < 200KB (gzipped)
  • Time to First Byte (TTFB): < 600ms
  • Speed Index: < 3.0s
  • Frame Rate: 60 FPS (16.67ms per frame)
  • Memory Usage: < 50MB baseline

🚀 Loading Performance

Code Splitting

// Dynamic imports for code splitting
class DemoLoader {
  constructor() {
    this.loadedModules = new Map();
  }

  async loadModule(moduleName) {
    // Check cache first
    if (this.loadedModules.has(moduleName)) {
      return this.loadedModules.get(moduleName);
    }

    // Dynamic import
    let module;
    switch (moduleName) {
      case 'chart':
        module = await import(/* webpackChunkName: "chart" */ './chart-module.js');
        break;
      case 'editor':
        module = await import(/* webpackChunkName: "editor" */ './editor-module.js');
        break;
      case 'calculator':
        module = await import(/* webpackChunkName: "calculator" */ './calculator-module.js');
        break;
      default:
        throw new Error(`Unknown module: ${moduleName}`);
    }

    // Cache the module
    this.loadedModules.set(moduleName, module);
    return module;
  }

  async loadOnDemand(trigger, moduleName) {
    // Load module when user interacts
    trigger.addEventListener('click', async () => {
      const module = await this.loadModule(moduleName);
      module.default.init(trigger);
    }, { once: true });
  }
}

// Usage
const loader = new DemoLoader();

// Load chart library only when needed
document.getElementById('show-chart-btn').addEventListener('click', async () => {
  const { ChartVisualizer } = await loader.loadModule('chart');
  const chart = new ChartVisualizer('chart-container');
  chart.render(data);
}, { once: true });

Resource Preloading

<!-- Preload critical resources -->
<head>
  <!-- Preload fonts -->
  <link rel="preload" href="/fonts/segoe-ui.woff2" as="font" type="font/woff2" crossorigin>

  <!-- Preload critical CSS -->
  <link rel="preload" href="/css/demo-critical.css" as="style">

  <!-- Preload critical scripts -->
  <link rel="preload" href="/js/demo-core.js" as="script">

  <!-- DNS prefetch for external resources -->
  <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">

  <!-- Preconnect to API endpoints -->
  <link rel="preconnect" href="https://api.azure.com" crossorigin>

  <!-- Critical CSS inline -->
  <style>
    /* Inline critical above-the-fold CSS */
    .demo-container { /* ... */ }
    .demo-header { /* ... */ }
  </style>

  <!-- Defer non-critical CSS -->
  <link rel="stylesheet" href="/css/demo-full.css" media="print" onload="this.media='all'">
</head>

Lazy Loading

// Lazy load images and components
class LazyLoader {
  constructor() {
    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersection(entries),
      {
        rootMargin: '50px',
        threshold: 0.01
      }
    );
  }

  observe(element) {
    this.observer.observe(element);
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.loadElement(entry.target);
        this.observer.unobserve(entry.target);
      }
    });
  }

  loadElement(element) {
    // Load images
    if (element.tagName === 'IMG') {
      const src = element.dataset.src;
      if (src) {
        element.src = src;
        element.removeAttribute('data-src');
      }
    }

    // Load components
    if (element.dataset.component) {
      this.loadComponent(element);
    }
  }

  async loadComponent(element) {
    const componentName = element.dataset.component;
    const module = await import(/* webpackChunkName: "[request]" */ `./components/${componentName}.js`);
    const component = new module.default(element);
    await component.init();
  }
}

// Initialize lazy loader
const lazyLoader = new LazyLoader();

// Lazy load images
document.querySelectorAll('img[data-src]').forEach(img => {
  lazyLoader.observe(img);
});

// Lazy load components
document.querySelectorAll('[data-component]').forEach(el => {
  lazyLoader.observe(el);
});

🎨 Rendering Performance

Virtual Scrolling

// Virtual scrolling for large lists
class VirtualScroller {
  constructor(container, items, options = {}) {
    this.container = container;
    this.items = items;
    this.options = {
      itemHeight: 50,
      overscan: 3,
      ...options
    };

    this.scrollTop = 0;
    this.visibleStart = 0;
    this.visibleEnd = 0;

    this.init();
  }

  init() {
    this.setupContainer();
    this.attachScrollListener();
    this.render();
  }

  setupContainer() {
    this.container.style.overflow = 'auto';
    this.container.style.position = 'relative';
    this.container.style.height = `${this.options.containerHeight}px`;

    // Create spacer for total height
    this.spacer = document.createElement('div');
    this.spacer.style.height = `${this.items.length * this.options.itemHeight}px`;
    this.container.appendChild(this.spacer);

    // Create viewport for visible items
    this.viewport = document.createElement('div');
    this.viewport.style.position = 'absolute';
    this.viewport.style.top = '0';
    this.viewport.style.left = '0';
    this.viewport.style.right = '0';
    this.container.appendChild(this.viewport);
  }

  attachScrollListener() {
    this.container.addEventListener('scroll', () => {
      this.scrollTop = this.container.scrollTop;
      this.render();
    });
  }

  render() {
    const containerHeight = this.container.clientHeight;

    // Calculate visible range
    const start = Math.floor(this.scrollTop / this.options.itemHeight);
    const end = Math.ceil((this.scrollTop + containerHeight) / this.options.itemHeight);

    // Add overscan
    this.visibleStart = Math.max(0, start - this.options.overscan);
    this.visibleEnd = Math.min(this.items.length, end + this.options.overscan);

    // Only update if range changed significantly
    if (this.shouldUpdate()) {
      this.updateViewport();
    }
  }

  shouldUpdate() {
    return Math.abs(this.lastStart - this.visibleStart) > 1 ||
           Math.abs(this.lastEnd - this.visibleEnd) > 1;
  }

  updateViewport() {
    this.lastStart = this.visibleStart;
    this.lastEnd = this.visibleEnd;

    // Clear viewport
    this.viewport.innerHTML = '';

    // Position viewport
    this.viewport.style.transform = `translateY(${this.visibleStart * this.options.itemHeight}px)`;

    // Render visible items
    const fragment = document.createDocumentFragment();

    for (let i = this.visibleStart; i < this.visibleEnd; i++) {
      const item = this.renderItem(this.items[i], i);
      fragment.appendChild(item);
    }

    this.viewport.appendChild(fragment);
  }

  renderItem(data, index) {
    const div = document.createElement('div');
    div.className = 'virtual-item';
    div.style.height = `${this.options.itemHeight}px`;
    div.textContent = data;
    div.dataset.index = index;
    return div;
  }
}

// Usage
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
const scroller = new VirtualScroller(
  document.getElementById('list-container'),
  items,
  {
    itemHeight: 50,
    containerHeight: 400,
    overscan: 5
  }
);

RequestAnimationFrame

// Smooth animations using RAF
class AnimationScheduler {
  constructor() {
    this.tasks = new Set();
    this.isRunning = false;
  }

  schedule(callback) {
    this.tasks.add(callback);

    if (!this.isRunning) {
      this.start();
    }

    // Return cancel function
    return () => this.tasks.delete(callback);
  }

  start() {
    this.isRunning = true;
    this.tick();
  }

  tick() {
    // Execute all tasks
    this.tasks.forEach(callback => {
      try {
        callback();
      } catch (error) {
        console.error('Animation task error:', error);
      }
    });

    // Continue if there are tasks
    if (this.tasks.size > 0) {
      requestAnimationFrame(() => this.tick());
    } else {
      this.isRunning = false;
    }
  }

  scheduleOnce(callback) {
    const wrapped = () => {
      callback();
      this.tasks.delete(wrapped);
    };
    this.schedule(wrapped);
  }
}

// Global scheduler
const scheduler = new AnimationScheduler();

// Example: Smooth value interpolation
function animateValue(element, start, end, duration) {
  const startTime = performance.now();

  const update = () => {
    const currentTime = performance.now();
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // Easing function
    const eased = progress < 0.5
      ? 2 * progress * progress
      : 1 - Math.pow(-2 * progress + 2, 2) / 2;

    const current = start + (end - start) * eased;
    element.textContent = Math.round(current);

    if (progress < 1) {
      requestAnimationFrame(update);
    }
  };

  requestAnimationFrame(update);
}

// Usage
animateValue(document.getElementById('cost-value'), 0, 2350, 1000);

Debouncing and Throttling

// Utility functions for performance
class PerformanceUtils {
  // Debounce - wait for pause in events
  static debounce(func, wait) {
    let timeoutId;

    return function debounced(...args) {
      clearTimeout(timeoutId);

      timeoutId = setTimeout(() => {
        func.apply(this, args);
      }, wait);
    };
  }

  // Throttle - limit execution frequency
  static throttle(func, limit) {
    let inThrottle;

    return function throttled(...args) {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;

        setTimeout(() => {
          inThrottle = false;
        }, limit);
      }
    };
  }

  // Request Animation Frame throttle
  static rafThrottle(func) {
    let rafId = null;

    return function throttled(...args) {
      if (rafId === null) {
        rafId = requestAnimationFrame(() => {
          func.apply(this, args);
          rafId = null;
        });
      }
    };
  }
}

// Usage examples

// Debounce search input
const searchInput = document.getElementById('search');
const handleSearch = PerformanceUtils.debounce((value) => {
  console.log('Searching for:', value);
  performSearch(value);
}, 300);

searchInput.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});

// Throttle scroll events
const handleScroll = PerformanceUtils.throttle(() => {
  console.log('Scroll position:', window.scrollY);
  updateScrollIndicator();
}, 100);

window.addEventListener('scroll', handleScroll);

// RAF throttle for resize
const handleResize = PerformanceUtils.rafThrottle(() => {
  console.log('Window resized:', window.innerWidth);
  updateLayout();
});

window.addEventListener('resize', handleResize);

💾 Data Management

Memoization

// Memoize expensive calculations
class MemoCache {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  memoize(fn) {
    return (...args) => {
      const key = JSON.stringify(args);

      if (this.cache.has(key)) {
        return this.cache.get(key);
      }

      const result = fn(...args);

      // Implement LRU eviction
      if (this.cache.size >= this.maxSize) {
        const firstKey = this.cache.keys().next().value;
        this.cache.delete(firstKey);
      }

      this.cache.set(key, result);
      return result;
    };
  }

  clear() {
    this.cache.clear();
  }
}

// Usage
const memoCache = new MemoCache();

const calculateCost = memoCache.memoize((nodeSize, nodeCount, hours) => {
  console.log('Calculating cost...'); // Only logs on cache miss

  const rates = {
    small: 0.10,
    medium: 0.20,
    large: 0.40
  };

  return rates[nodeSize] * nodeCount * hours;
});

// First call - computes
console.log(calculateCost('medium', 5, 720)); // Calculates

// Second call with same args - cached
console.log(calculateCost('medium', 5, 720)); // Returns cached value

// Different args - computes
console.log(calculateCost('large', 10, 720)); // Calculates

Data Pagination

// Paginated data loading
class DataPaginator {
  constructor(dataSource, pageSize = 50) {
    this.dataSource = dataSource;
    this.pageSize = pageSize;
    this.currentPage = 0;
    this.cache = new Map();
    this.totalItems = 0;
  }

  async loadPage(page) {
    // Check cache
    if (this.cache.has(page)) {
      return this.cache.get(page);
    }

    // Load from data source
    const offset = page * this.pageSize;
    const data = await this.dataSource.fetch(offset, this.pageSize);

    // Update total
    if (data.total !== undefined) {
      this.totalItems = data.total;
    }

    // Cache the page
    this.cache.set(page, data.items);

    return data.items;
  }

  async next() {
    const maxPage = Math.ceil(this.totalItems / this.pageSize) - 1;

    if (this.currentPage < maxPage) {
      this.currentPage++;
      return await this.loadPage(this.currentPage);
    }

    return null;
  }

  async previous() {
    if (this.currentPage > 0) {
      this.currentPage--;
      return await this.loadPage(this.currentPage);
    }

    return null;
  }

  async goToPage(page) {
    this.currentPage = page;
    return await this.loadPage(page);
  }

  getTotalPages() {
    return Math.ceil(this.totalItems / this.pageSize);
  }

  clearCache() {
    this.cache.clear();
  }
}

// Usage
const dataSource = {
  async fetch(offset, limit) {
    const response = await fetch(`/api/data?offset=${offset}&limit=${limit}`);
    return await response.json();
  }
};

const paginator = new DataPaginator(dataSource, 50);

// Load first page
const items = await paginator.loadPage(0);
renderItems(items);

// Next page
document.getElementById('next-btn').addEventListener('click', async () => {
  const items = await paginator.next();
  if (items) {
    renderItems(items);
  }
});

Caching Strategy

// Multi-layer caching
class CacheManager {
  constructor() {
    this.memoryCache = new Map();
    this.storageCache = window.localStorage;
    this.ttl = 5 * 60 * 1000; // 5 minutes
  }

  set(key, value, ttl = this.ttl) {
    const item = {
      value,
      expires: Date.now() + ttl
    };

    // Memory cache
    this.memoryCache.set(key, item);

    // Storage cache (with error handling)
    try {
      this.storageCache.setItem(key, JSON.stringify(item));
    } catch (error) {
      console.warn('Storage cache failed:', error);
    }
  }

  get(key) {
    // Try memory cache first
    let item = this.memoryCache.get(key);

    // Try storage cache
    if (!item) {
      try {
        const stored = this.storageCache.getItem(key);
        if (stored) {
          item = JSON.parse(stored);
          // Restore to memory cache
          this.memoryCache.set(key, item);
        }
      } catch (error) {
        console.warn('Storage cache read failed:', error);
      }
    }

    // Check expiration
    if (item) {
      if (Date.now() < item.expires) {
        return item.value;
      } else {
        this.delete(key);
      }
    }

    return null;
  }

  delete(key) {
    this.memoryCache.delete(key);
    try {
      this.storageCache.removeItem(key);
    } catch (error) {
      console.warn('Storage cache delete failed:', error);
    }
  }

  clear() {
    this.memoryCache.clear();
    try {
      const keys = Object.keys(this.storageCache);
      keys.forEach(key => this.storageCache.removeItem(key));
    } catch (error) {
      console.warn('Storage cache clear failed:', error);
    }
  }

  async getOrFetch(key, fetchFn, ttl) {
    // Try cache first
    const cached = this.get(key);
    if (cached !== null) {
      return cached;
    }

    // Fetch and cache
    const value = await fetchFn();
    this.set(key, value, ttl);
    return value;
  }
}

// Usage
const cache = new CacheManager();

async function getSparkPools() {
  return await cache.getOrFetch(
    'spark-pools',
    async () => {
      const response = await fetch('/api/spark-pools');
      return await response.json();
    },
    10 * 60 * 1000 // 10 minutes
  );
}

🔧 Bundle Optimization

Tree Shaking

// package.json
{
  "sideEffects": false,
  "module": "dist/demo.esm.js"
}

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    minimize: true,
    sideEffects: true
  }
};

// Use ES6 imports for tree shaking
import { calculateCost } from './utils';

// Avoid default exports when possible
export const calculateCost = () => { /* ... */ };
export const formatCurrency = () => { /* ... */ };

Compression

// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    }),
    new CompressionPlugin({
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    })
  ]
};

Asset Optimization

// Image optimization
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = {
  plugins: [
    new ImageMinimizerPlugin({
      minimizer: {
        implementation: ImageMinimizerPlugin.imageminGenerate,
        options: {
          plugins: [
            ['imagemin-mozjpeg', { quality: 80 }],
            ['imagemin-pngquant', { quality: [0.65, 0.90] }],
            ['imagemin-svgo', {
              plugins: [{
                name: 'removeViewBox',
                active: false
              }]
            }]
          ]
        }
      }
    })
  ]
};

📊 Performance Monitoring

Performance API

// Performance monitoring
class PerformanceMonitor {
  constructor() {
    this.marks = new Map();
    this.measures = [];
  }

  mark(name) {
    performance.mark(name);
    this.marks.set(name, performance.now());
  }

  measure(name, startMark, endMark) {
    if (!endMark) {
      endMark = `${startMark}-end`;
      this.mark(endMark);
    }

    performance.measure(name, startMark, endMark);

    const measure = performance.getEntriesByName(name, 'measure')[0];
    this.measures.push({
      name,
      duration: measure.duration,
      timestamp: Date.now()
    });

    return measure.duration;
  }

  getMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0];
    const paint = performance.getEntriesByType('paint');

    return {
      // Navigation Timing
      dns: navigation.domainLookupEnd - navigation.domainLookupStart,
      tcp: navigation.connectEnd - navigation.connectStart,
      request: navigation.responseStart - navigation.requestStart,
      response: navigation.responseEnd - navigation.responseStart,
      domProcessing: navigation.domComplete - navigation.domLoading,
      totalTime: navigation.loadEventEnd - navigation.fetchStart,

      // Paint Timing
      fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
      lcp: this.getLCP(),

      // Custom measures
      customMeasures: this.measures
    };
  }

  getLCP() {
    return new Promise((resolve) => {
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1];
        resolve(lastEntry.startTime);
      }).observe({ entryTypes: ['largest-contentful-paint'] });

      // Timeout after 5 seconds
      setTimeout(() => resolve(null), 5000);
    });
  }

  getCLS() {
    return new Promise((resolve) => {
      let clsScore = 0;

      new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (!entry.hadRecentInput) {
            clsScore += entry.value;
          }
        }
      }).observe({ entryTypes: ['layout-shift'] });

      // Resolve after page interaction stops
      setTimeout(() => resolve(clsScore), 5000);
    });
  }

  async reportMetrics() {
    const metrics = this.getMetrics();
    metrics.cls = await this.getCLS();

    // Send to analytics
    if (window.gtag) {
      window.gtag('event', 'performance_metrics', metrics);
    }

    console.table(metrics);
    return metrics;
  }
}

// Usage
const monitor = new PerformanceMonitor();

// Mark important events
monitor.mark('demo-start');
// ... demo initialization
monitor.mark('demo-ready');
monitor.measure('demo-initialization', 'demo-start', 'demo-ready');

// Report on page load
window.addEventListener('load', async () => {
  await monitor.reportMetrics();
});

Real User Monitoring (RUM)

// RUM integration
class RUMCollector {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.data = {
      pageUrl: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      metrics: {},
      errors: []
    };

    this.init();
  }

  init() {
    this.collectMetrics();
    this.trackErrors();
    this.trackInteractions();

    // Send on page unload
    window.addEventListener('beforeunload', () => {
      this.send();
    });
  }

  collectMetrics() {
    if (window.PerformanceObserver) {
      // Observe LCP
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        this.data.metrics.lcp = entries[entries.length - 1].startTime;
      }).observe({ entryTypes: ['largest-contentful-paint'] });

      // Observe FID
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        this.data.metrics.fid = entries[0].processingStart - entries[0].startTime;
      }).observe({ entryTypes: ['first-input'] });

      // Observe CLS
      let clsScore = 0;
      new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (!entry.hadRecentInput) {
            clsScore += entry.value;
          }
        }
        this.data.metrics.cls = clsScore;
      }).observe({ entryTypes: ['layout-shift'] });
    }
  }

  trackErrors() {
    window.addEventListener('error', (event) => {
      this.data.errors.push({
        message: event.message,
        filename: event.filename,
        line: event.lineno,
        column: event.colno,
        timestamp: Date.now()
      });
    });

    window.addEventListener('unhandledrejection', (event) => {
      this.data.errors.push({
        type: 'unhandledRejection',
        reason: event.reason?.toString(),
        timestamp: Date.now()
      });
    });
  }

  trackInteractions() {
    ['click', 'input', 'scroll'].forEach(eventType => {
      document.addEventListener(eventType, () => {
        if (!this.data.metrics.firstInteraction) {
          this.data.metrics.firstInteraction = performance.now();
        }
      }, { once: true, passive: true });
    });
  }

  send() {
    // Use sendBeacon for reliable delivery
    if (navigator.sendBeacon) {
      const blob = new Blob([JSON.stringify(this.data)], {
        type: 'application/json'
      });
      navigator.sendBeacon(this.endpoint, blob);
    } else {
      // Fallback to fetch with keepalive
      fetch(this.endpoint, {
        method: 'POST',
        body: JSON.stringify(this.data),
        headers: { 'Content-Type': 'application/json' },
        keepalive: true
      }).catch(error => console.error('RUM send failed:', error));
    }
  }
}

// Initialize RUM
const rum = new RUMCollector('/api/rum');

📚 Resources

Performance Tools

Documentation


Last Updated: January 2025 | Version: 1.0.0