Performance

6 min read
Rapid overview

JavaScript Performance Optimization

Performance best practices and optimization techniques for JavaScript applications.

Table of Contents

Memory Management

Avoiding Memory Leaks

Common Memory Leak Causes:

  1. Global Variables:
// ❌ Bad - creates global variable
function leak() {
  undeclaredVar = 'leaks to global scope';
}

// ✅ Good - use strict mode and proper declarations
'use strict';
function noLeak() {
  const declaredVar = 'properly scoped';
}
  1. Event Listeners:
// ❌ Bad - doesn't remove listener
const button = document.getElementById('btn');
button.addEventListener('click', handleClick);

// ✅ Good - remove when done
function cleanup() {
  button.removeEventListener('click', handleClick);
}
  1. Timers:
// ❌ Bad - timer keeps running
const intervalId = setInterval(() => {
  console.log('Running...');
}, 1000);

// ✅ Good - clear when done
clearInterval(intervalId);
  1. Closures Holding References:
// ❌ Bad - closure holds large object
function createHandler() {
  const largeObject = new Array(1000000);
  return function() {
    console.log(largeObject.length);
  };
}

// ✅ Good - only store what you need
function createHandler() {
  const length = new Array(1000000).length;
  return function() {
    console.log(length);
  };
}

Garbage Collection

// Help GC by nullifying references
let largeObject = { /* ... */ };
// ... use largeObject ...
largeObject = null; // Allow GC to reclaim memory

WeakMap and WeakSet

// WeakMap allows garbage collection of keys
const cache = new WeakMap();
let obj = { data: 'important' };

cache.set(obj, 'metadata');

// When obj is no longer referenced elsewhere,
// both the key and value can be garbage collected
obj = null;

// Regular Map would prevent GC
const regularCache = new Map();
// Keys won't be garbage collected even if not used

DOM Manipulation

Minimize Reflows and Repaints

// ❌ Bad - causes multiple reflows
const element = document.getElementById('item');
element.style.width = '100px';  // reflow
element.style.height = '100px'; // reflow
element.style.margin = '10px';  // reflow

// ✅ Good - batch changes
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

// Or use classes
element.className = 'optimized-style';

Document Fragments

// ❌ Bad - multiple DOM manipulations
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  list.appendChild(li); // causes reflow each time
}

// ✅ Good - use DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
list.appendChild(fragment); // single reflow

Read/Write Batching

// ❌ Bad - interleaved reads and writes (thrashing)
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
  const height = el.offsetHeight; // read (forces layout)
  el.style.height = (height + 10) + 'px'; // write
});

// ✅ Good - batch reads, then writes
const heights = Array.from(elements).map(el => el.offsetHeight);
elements.forEach((el, i) => {
  el.style.height = (heights[i] + 10) + 'px';
});

Event Handling

Event Delegation

// ❌ Bad - multiple event listeners
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick);
});

// ✅ Good - single delegated listener
document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.classList.contains('item')) {
    handleClick(e);
  }
});

Debouncing

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

// Usage
const handleResize = debounce(() => {
  console.log('Window resized');
}, 250);

window.addEventListener('resize', handleResize);

Throttling

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

// Usage
const handleScroll = throttle(() => {
  console.log('Scrolling...');
}, 100);

window.addEventListener('scroll', handleScroll);

Passive Event Listeners

// ✅ Good - improves scroll performance
document.addEventListener('touchstart', handler, { passive: true });
document.addEventListener('wheel', handler, { passive: true });

Loops and Iterations

Array Methods Performance

const arr = new Array(1000000).fill(0);

// Fastest
for (let i = 0; i < arr.length; i++) { /* ... */ }

// Slightly slower but more readable
for (const item of arr) { /* ... */ }

// Slowest but most expressive
arr.forEach(item => { /* ... */ });

// For filtering/mapping, native methods are optimized
const doubled = arr.map(x => x * 2);
const evens = arr.filter(x => x % 2 === 0);

Caching Length

// ❌ Bad - recalculates length each iteration
for (let i = 0; i < array.length; i++) {
  // ...
}

// ✅ Good - cache length
const len = array.length;
for (let i = 0; i < len; i++) {
  // ...
}

Early Termination

// ✅ Use early returns/breaks
function findItem(arr, predicate) {
  for (let i = 0; i < arr.length; i++) {
    if (predicate(arr[i])) {
      return arr[i]; // stop as soon as found
    }
  }
  return null;
}

// ✅ Use .some() for existence checks
const hasEven = arr.some(x => x % 2 === 0);

// ✅ Use .find() instead of .filter()[0]
const firstEven = arr.find(x => x % 2 === 0);

Async Operations

Parallel vs Sequential

// ❌ Sequential - slow
async function sequential() {
  const result1 = await fetch('/api/1');
  const result2 = await fetch('/api/2');
  const result3 = await fetch('/api/3');
  return [result1, result2, result3];
}

// ✅ Parallel - fast
async function parallel() {
  const [result1, result2, result3] = await Promise.all([
    fetch('/api/1'),
    fetch('/api/2'),
    fetch('/api/3')
  ]);
  return [result1, result2, result3];
}

Request Batching

class RequestBatcher {
  constructor(batchFn, delay = 10) {
    this.batchFn = batchFn;
    this.delay = delay;
    this.queue = [];
    this.timeoutId = null;
  }

  request(id) {
    return new Promise((resolve, reject) => {
      this.queue.push({ id, resolve, reject });

      if (!this.timeoutId) {
        this.timeoutId = setTimeout(() => {
          this.flush();
        }, this.delay);
      }
    });
  }

  flush() {
    const batch = this.queue.splice(0);
    this.timeoutId = null;

    const ids = batch.map(item => item.id);
    this.batchFn(ids)
      .then(results => {
        batch.forEach((item, i) => {
          item.resolve(results[i]);
        });
      })
      .catch(error => {
        batch.forEach(item => item.reject(error));
      });
  }
}

// Usage
const batcher = new RequestBatcher(async (ids) => {
  const response = await fetch(`/api/items?ids=${ids.join(',')}`);
  return response.json();
});

batcher.request(1);
batcher.request(2);
batcher.request(3);
// All three requests batched into single API call

Caching Promises

const promiseCache = new Map();

function cachedFetch(url) {
  if (promiseCache.has(url)) {
    return promiseCache.get(url);
  }

  const promise = fetch(url).then(res => res.json());
  promiseCache.set(url, promise);

  return promise;
}

Code Splitting and Lazy Loading

Dynamic Imports

// Load module only when needed
async function loadFeature() {
  const module = await import('./heavy-feature.js');
  module.initialize();
}

// Conditional loading
if (userWantsDarkMode) {
  import('./dark-theme.js').then(theme => theme.apply());
}

Intersection Observer for Lazy Loading

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

Measuring Performance

Performance API

// Measure function execution time
performance.mark('start');
expensiveOperation();
performance.mark('end');
performance.measure('operation', 'start', 'end');

const measure = performance.getEntriesByName('operation')[0];
console.log(`Operation took ${measure.duration}ms`);

// Clean up
performance.clearMarks();
performance.clearMeasures();

User Timing API

function measureAsync() {
  performance.mark('fetch-start');

  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      performance.mark('fetch-end');
      performance.measure('fetch-duration', 'fetch-start', 'fetch-end');

      const measure = performance.getEntriesByName('fetch-duration')[0];
      console.log(`Fetch took ${measure.duration}ms`);
    });
}

Console Timing

console.time('operation');
expensiveOperation();
console.timeEnd('operation');
// Logs: operation: 123.456ms

Memory Profiling

// Check memory usage (Chrome only)
if (performance.memory) {
  console.log({
    usedJSHeapSize: performance.memory.usedJSHeapSize / 1048576 + ' MB',
    totalJSHeapSize: performance.memory.totalJSHeapSize / 1048576 + ' MB',
    jsHeapSizeLimit: performance.memory.jsHeapSizeLimit / 1048576 + ' MB'
  });
}

Best Practices Summary

  1. Minimize DOM Access - Cache DOM references
  2. Batch DOM Changes - Use fragments, cssText, classes
  3. Debounce/Throttle Events - For scroll, resize, input
  4. Use Event Delegation - One listener instead of many
  5. Lazy Load Resources - Load only what's needed
  6. Cache Results - Memoize expensive computations
  7. Optimize Loops - Cache length, use appropriate methods
  8. Parallel Async Operations - Use Promise.all()
  9. Avoid Memory Leaks - Remove listeners, clear timers
  10. Measure Performance - Use Performance API to identify bottlenecks

Tools for Performance Analysis

  • Chrome DevTools Performance Panel
  • Lighthouse
  • WebPageTest
  • Performance API
  • Chrome DevTools Memory Profiler
  • React DevTools Profiler (for React apps)
  • Angular DevTools (for Angular apps)