返回博客

10 JavaScript Performance Optimization Techniques You Need to Know

Master essential JavaScript performance optimization techniques to make your web applications faster. Learn debouncing, lazy loading, code splitting, and more with practical examples.

11 min read
作者:Perf Lens Team

Performance is crucial for modern web applications. Studies show that even a 100ms delay can impact user engagement and conversion rates. In this comprehensive guide, we'll explore 10 essential JavaScript performance optimization techniques that can significantly improve your application's speed and user experience.

1. Debouncing and Throttling

The Problem

Frequent event handlers (like scroll, resize, or input events) can fire hundreds of times per second, causing performance bottlenecks.

The Solution: Debouncing

Debouncing delays function execution until after a period of inactivity:

/**
 * Debounce function - delays execution until after wait time
 * @param {Function} func - Function to debounce
 * @param {number} wait - Wait time in milliseconds
 */
function debounce(func, wait) {
  let timeout;

  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Usage: Search input
const searchInput = document.getElementById('search');
const performSearch = debounce((query) => {
  console.log('Searching for:', query);
  // API call here
}, 300);

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

The Solution: Throttling

Throttling ensures a function runs at most once per specified time period:

/**
 * Throttle function - limits execution to once per wait period
 * @param {Function} func - Function to throttle
 * @param {number} wait - Wait time in milliseconds
 */
function throttle(func, wait) {
  let lastTime = 0;

  return function executedFunction(...args) {
    const now = Date.now();

    if (now - lastTime >= wait) {
      lastTime = now;
      func(...args);
    }
  };
}

// Usage: Scroll event
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
  // Update UI based on scroll
}, 100);

window.addEventListener('scroll', handleScroll);

Performance Impact: Reduces event handler calls by 90-99%, saving CPU cycles and preventing UI jank.


2. Lazy Loading Images and Components

The Problem

Loading all images and components upfront increases initial page load time and wastes bandwidth.

The Solution: Native Lazy Loading

Modern browsers support native lazy loading:

<!-- Native lazy loading -->
<img
  src="hero-image.jpg"
  alt="Hero"
  loading="lazy"
  width="800"
  height="600"
/>

Advanced: Intersection Observer API

For more control, use the Intersection Observer API:

/**
 * Lazy load images using Intersection Observer
 */
class LazyImageLoader {
  constructor(options = {}) {
    this.options = {
      root: null,
      rootMargin: '50px',
      threshold: 0.01,
      ...options
    };

    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      this.options
    );
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        const src = img.dataset.src;

        if (src) {
          img.src = src;
          img.removeAttribute('data-src');
          this.observer.unobserve(img);
        }
      }
    });
  }

  observe(images) {
    images.forEach(img => this.observer.observe(img));
  }
}

// Usage
const lazyLoader = new LazyImageLoader();
const images = document.querySelectorAll('img[data-src]');
lazyLoader.observe(images);

Performance Impact: Reduces initial page load by 40-60% for image-heavy pages.


3. Code Splitting and Dynamic Imports

The Problem

Large JavaScript bundles slow down initial page load and Time to Interactive (TTI).

The Solution: Dynamic Imports

Split code into smaller chunks and load on demand:

// Before: Large bundle
import HeavyChart from './heavy-chart.js';
import HeavyEditor from './heavy-editor.js';

// After: Dynamic imports
async function loadChart() {
  const { default: HeavyChart } = await import('./heavy-chart.js');
  const chart = new HeavyChart();
  chart.render();
}

async function loadEditor() {
  const { default: HeavyEditor } = await import('./heavy-editor.js');
  const editor = new HeavyEditor();
  editor.init();
}

// Load only when needed
document.getElementById('show-chart').addEventListener('click', loadChart);
document.getElementById('show-editor').addEventListener('click', loadEditor);

React Example

import { lazy, Suspense } from 'react';

// Lazy load components
const HeavyChart = lazy(() => import('./HeavyChart'));
const HeavyEditor = lazy(() => import('./HeavyEditor'));

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

      <Suspense fallback={<div>Loading editor...</div>}>
        <HeavyEditor />
      </Suspense>
    </div>
  );
}

Performance Impact: Reduces initial bundle size by 30-70%, improving TTI significantly.


4. Memoization and Caching

The Problem

Expensive computations repeated with the same inputs waste CPU cycles.

The Solution: Memoization

Cache function results for repeated calls:

/**
 * Memoize function - caches results based on arguments
 * @param {Function} fn - Function to memoize
 */
function memoize(fn) {
  const cache = new Map();

  return function memoized(...args) {
    const key = JSON.stringify(args);

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

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Example: Expensive calculation
const fibonacci = memoize((n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.time('First call');
console.log(fibonacci(40)); // ~1000ms
console.timeEnd('First call');

console.time('Cached call');
console.log(fibonacci(40)); // <1ms
console.timeEnd('Cached call');

React useMemo Example

import { useMemo } from 'react';

function ExpensiveComponent({ data }) {
  // Memoize expensive calculation
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return data.map(item => ({
      ...item,
      computed: expensiveOperation(item)
    }));
  }, [data]); // Only recompute when data changes

  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>{item.computed}</div>
      ))}
    </div>
  );
}

Performance Impact: 100-1000x faster for repeated calculations with the same inputs.


5. Virtual Scrolling for Large Lists

The Problem

Rendering thousands of list items causes slow rendering and high memory usage.

The Solution: Virtual Scrolling

Only render visible items:

/**
 * Simple virtual scroll implementation
 */
class VirtualScroll {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
    this.scrollTop = 0;

    this.init();
  }

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

    // Create visible items container
    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.width = '100%';
    this.container.appendChild(this.content);

    // Listen to scroll
    this.container.addEventListener('scroll', () => this.onScroll());

    // Initial render
    this.render();
  }

  onScroll() {
    this.scrollTop = this.container.scrollTop;
    this.render();
  }

  render() {
    const startIndex = Math.floor(this.scrollTop / this.itemHeight);
    const endIndex = Math.min(
      startIndex + this.visibleCount + 1,
      this.items.length
    );

    // Update content position
    this.content.style.transform = `translateY(${startIndex * this.itemHeight}px)`;

    // Render visible items
    this.content.innerHTML = '';
    for (let i = startIndex; i < endIndex; i++) {
      const item = document.createElement('div');
      item.style.height = `${this.itemHeight}px`;
      item.textContent = this.items[i];
      this.content.appendChild(item);
    }
  }
}

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

Performance Impact: Reduces DOM nodes from 10,000+ to ~20, improving render time by 95%+.


6. Web Workers for Heavy Computations

The Problem

Heavy computations block the main thread, freezing the UI.

The Solution: Web Workers

Offload work to background threads:

// worker.js
self.addEventListener('message', (e) => {
  const { type, data } = e.data;

  if (type === 'PROCESS_DATA') {
    // Heavy computation
    const result = processLargeDataset(data);
    self.postMessage({ type: 'RESULT', result });
  }
});

function processLargeDataset(data) {
  // Simulate heavy work
  return data.map(item => ({
    ...item,
    processed: expensiveOperation(item)
  }));
}

// main.js
const worker = new Worker('worker.js');

worker.addEventListener('message', (e) => {
  const { type, result } = e.data;

  if (type === 'RESULT') {
    console.log('Result from worker:', result);
    updateUI(result);
  }
});

// Send work to worker
function processData(data) {
  worker.postMessage({ type: 'PROCESS_DATA', data });
}

Performance Impact: Keeps UI responsive during heavy computations, preventing jank.


7. RequestAnimationFrame for Animations

The Problem

Using setTimeout or setInterval for animations causes jank and wastes battery.

The Solution: requestAnimationFrame

Synchronize with the browser's repaint cycle:

// ❌ Bad: setTimeout
function animateWithTimeout() {
  let position = 0;

  const animate = () => {
    position += 2;
    element.style.transform = `translateX(${position}px)`;

    if (position < 500) {
      setTimeout(animate, 16); // ~60fps, but not synced
    }
  };

  animate();
}

// ✅ Good: requestAnimationFrame
function animateWithRAF() {
  let position = 0;
  let lastTime = performance.now();

  const animate = (currentTime) => {
    const deltaTime = currentTime - lastTime;
    lastTime = currentTime;

    // Time-based animation (frame-independent)
    position += 0.2 * deltaTime; // 200px per second
    element.style.transform = `translateX(${position}px)`;

    if (position < 500) {
      requestAnimationFrame(animate);
    }
  };

  requestAnimationFrame(animate);
}

Performance Impact: Smoother animations, better battery life, automatic pause when tab is hidden.


8. Avoid Memory Leaks

Common Causes

  1. Event listeners not removed
  2. Global variables
  3. Detached DOM nodes
  4. Closures holding references

Best Practices

// ✅ Clean up event listeners
class Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }

  mount() {
    document.addEventListener('click', this.handleClick);
  }

  unmount() {
    // Clean up!
    document.removeEventListener('click', this.handleClick);
  }

  handleClick(e) {
    console.log('Clicked:', e.target);
  }
}

// ✅ Use WeakMap for metadata
const metadata = new WeakMap();

function setMetadata(obj, data) {
  metadata.set(obj, data); // Automatically garbage collected with obj
}

// ✅ Clear timers and intervals
class Timer {
  start() {
    this.interval = setInterval(() => {
      console.log('Tick');
    }, 1000);
  }

  stop() {
    clearInterval(this.interval); // Clean up!
  }
}

Performance Impact: Prevents memory bloat that degrades performance over time.


9. Optimize DOM Manipulation

The Problem

Frequent DOM updates cause layout thrashing and reflows.

The Solution: Batch DOM Updates

// ❌ Bad: Multiple reflows
function badDOMUpdate(items) {
  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item;
    document.body.appendChild(div); // Reflow on each append!
  });
}

// ✅ Good: Single reflow
function goodDOMUpdate(items) {
  const fragment = document.createDocumentFragment();

  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item;
    fragment.appendChild(div); // No reflow
  });

  document.body.appendChild(fragment); // Single reflow
}

// ✅ Better: Use innerHTML for large updates
function betterDOMUpdate(items) {
  const html = items.map(item => `<div>${item}</div>`).join('');
  document.body.innerHTML += html; // Single reflow
}

Avoid Layout Thrashing

// ❌ Bad: Read-write-read-write (layout thrashing)
function badLayout() {
  const height1 = element1.offsetHeight; // Read (forces layout)
  element1.style.height = '100px';       // Write

  const height2 = element2.offsetHeight; // Read (forces layout again!)
  element2.style.height = '200px';       // Write
}

// ✅ Good: Batch reads, then batch writes
function goodLayout() {
  // Batch reads
  const height1 = element1.offsetHeight;
  const height2 = element2.offsetHeight;

  // Batch writes
  element1.style.height = '100px';
  element2.style.height = '200px';
}

Performance Impact: 5-10x faster DOM updates, eliminates layout thrashing.


10. Use Performance API for Monitoring

Measure Real Performance

/**
 * Performance monitoring utility
 */
class PerformanceMonitor {
  static mark(name) {
    performance.mark(name);
  }

  static measure(name, startMark, endMark) {
    performance.measure(name, startMark, endMark);

    const measure = performance.getEntriesByName(name)[0];
    console.log(`${name}: ${measure.duration.toFixed(2)}ms`);

    return measure.duration;
  }

  static async measureAsync(name, fn) {
    const startMark = `${name}-start`;
    const endMark = `${name}-end`;

    this.mark(startMark);

    try {
      const result = await fn();
      this.mark(endMark);
      this.measure(name, startMark, endMark);
      return result;
    } catch (error) {
      this.mark(endMark);
      this.measure(name, startMark, endMark);
      throw error;
    }
  }
}

// Usage
PerformanceMonitor.mark('data-fetch-start');

fetch('/api/data')
  .then(res => res.json())
  .then(data => {
    PerformanceMonitor.mark('data-fetch-end');
    PerformanceMonitor.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');
  });

// Or with async/await
const data = await PerformanceMonitor.measureAsync('api-call', async () => {
  const res = await fetch('/api/data');
  return res.json();
});

Performance Impact: Identifies bottlenecks, enables data-driven optimization.


Performance Testing with Perf Lens

Want to measure the impact of these optimizations? Try Perf Lens - our free tool for testing JavaScript performance:

// Test your optimization
// Before optimization
function unoptimizedLoop(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

// After optimization
function optimizedLoop(arr) {
  let sum = 0;
  const len = arr.length; // Cache length
  for (let i = 0; i < len; i++) {
    sum += arr[i];
  }
  return sum;
}

Summary

These 10 optimization techniques can dramatically improve your JavaScript application's performance:

  1. Debouncing/Throttling: Reduce event handler calls by 90%+
  2. Lazy Loading: Cut initial load time by 40-60%
  3. Code Splitting: Reduce bundle size by 30-70%
  4. Memoization: 100-1000x faster repeated calculations
  5. Virtual Scrolling: 95%+ fewer DOM nodes for large lists
  6. Web Workers: Keep UI responsive during heavy work
  7. RequestAnimationFrame: Smooth, battery-efficient animations
  8. Avoid Leaks: Prevent memory bloat over time
  9. Optimize DOM: 5-10x faster DOM updates
  10. Monitor Performance: Data-driven optimization

Best Practices Checklist

  • ✅ Profile before optimizing (measure first!)
  • ✅ Focus on user-perceived performance (LCP, FID, CLS)
  • ✅ Test on low-end devices and slow networks
  • ✅ Use performance budgets
  • ✅ Monitor real user metrics (RUM)
  • ✅ Optimize for the critical rendering path
  • ✅ Defer non-critical JavaScript
  • ✅ Use modern browser APIs
  • ✅ Keep dependencies minimal
  • ✅ Test performance regularly

Resources


Want to test these techniques? Try Perf Lens to measure execution time, memory usage, and bundle size of your code.

10 JavaScript Performance Optimization Techniques You Need to Know | Perf Lens