JavaScript Performance Optimization - Complete Guide

Performance optimization is critical for creating fast, responsive web applications. This guide covers techniques, patterns, and best practices for optimizing JavaScript performance.

Measuring Performance #

Performance API #

// Measure execution time
const start = performance.now();

// Your code here
for (let i = 0; i < 1000000; i++) {
  // ...
}

const end = performance.now();
console.log(`Execution time: ${end - start}ms`);

console.time() #

console.time('operation');

// Your code
someOperation();

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

Performance Marks #

performance.mark('start');

// Your code
doSomething();

performance.mark('end');
performance.measure('operation', 'start', 'end');

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

Algorithm Optimization #

Time Complexity #

// Bad - O(n²)
function hasDuplicate(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) return true;
    }
  }
  return false;
}

// Good - O(n)
function hasDuplicate(arr) {
  const seen = new Set();
  for (const item of arr) {
    if (seen.has(item)) return true;
    seen.add(item);
  }
  return false;
}

Memoization #

// Without memoization - exponential time
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// With memoization - linear time
function fibonacci(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;

  memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
  return memo[n];
}

// Using closure
function createFibonacci() {
  const cache = {};

  return function fib(n) {
    if (n in cache) return cache[n];
    if (n <= 1) return n;
    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  };
}

const fib = createFibonacci();
console.log(fib(40));  // Fast!

Debouncing #

function debounce(func, delay) {
  let timeoutId;

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

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

// Usage
const handleSearch = debounce((query) => {
  console.log('Searching for:', query);
}, 300);

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

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('Scroll position:', window.scrollY);
}, 100);

window.addEventListener('scroll', handleScroll);

Loop Optimization #

Cache Length #

// Bad
for (let i = 0; i < arr.length; i++) {
  // arr.length computed every iteration
}

// Good
for (let i = 0, len = arr.length; i < len; i++) {
  // Length cached
}

Use Appropriate Loop #

const arr = [1, 2, 3, 4, 5];

// forEach - No break/continue
arr.forEach(item => console.log(item));

// for...of - Can break/continue
for (const item of arr) {
  if (item === 3) break;
  console.log(item);
}

// for - Fastest for numeric iteration
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

// while - Best for unknown iterations
let i = 0;
while (i < arr.length) {
  console.log(arr[i++]);
}

Avoid Array Methods in Hot Paths #

// Bad - Creates new array
function sumSquares(arr) {
  return arr
    .map(x => x * x)
    .reduce((sum, x) => sum + x, 0);
}

// Good - Single pass
function sumSquares(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i] * arr[i];
  }
  return sum;
}

Object Optimization #

Object Creation #

// Bad - Dynamic property addition
const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;

// Good - All properties defined upfront
const obj = { a: 1, b: 2, c: 3 };

// Good - Constructor
function Point(x, y) {
  this.x = x;
  this.y = y;
}

const p = new Point(1, 2);

Object.freeze() for Constants #

// Prevents modifications and enables optimizations
const CONFIG = Object.freeze({
  API_URL: 'https://api.example.com',
  TIMEOUT: 5000,
  MAX_RETRIES: 3
});

Use Maps for Frequent Lookups #

// Bad - Object lookup
const cache = {};
cache[key] = value;
const result = cache[key];

// Good - Map is optimized for frequent additions/deletions
const cache = new Map();
cache.set(key, value);
const result = cache.get(key);

String Optimization #

Avoid String Concatenation in Loops #

// Bad
let str = '';
for (let i = 0; i < 1000; i++) {
  str += i;  // Creates new string each iteration
}

// Good
const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}
const str = arr.join('');

// Best - Template literal for known strings
const str = `${val1} ${val2} ${val3}`;

Use String Methods Wisely #

// Bad - Multiple operations
const result = str
  .toLowerCase()
  .trim()
  .split(' ')
  .join('-');

// Better - Regex for complex transformations
const result = str.toLowerCase().trim().replace(/\s+/g, '-');

Array Optimization #

Pre-allocate Arrays #

// Bad - Array grows dynamically
const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}

// Good - Pre-allocate size
const arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
  arr[i] = i;
}

Avoid Array Holes #

// Bad - Creates sparse array
const arr = [];
arr[1000] = 1;

// Good - Dense array
const arr = new Array(1001).fill(0);
arr[1000] = 1;

Use Typed Arrays for Numbers #

// Regular array
const arr = new Array(1000000);

// Typed array - More memory efficient
const arr = new Float64Array(1000000);
const arr = new Int32Array(1000000);

Function Optimization #

Avoid Creating Functions in Loops #

// Bad
for (let i = 0; i < arr.length; i++) {
  arr[i].addEventListener('click', function() {
    console.log(i);
  });
}

// Good
function handleClick(i) {
  return function() {
    console.log(i);
  };
}

for (let i = 0; i < arr.length; i++) {
  arr[i].addEventListener('click', handleClick(i));
}

// Best - Single handler
function handleClick(e) {
  const index = parseInt(e.target.dataset.index);
  console.log(index);
}

for (let i = 0; i < arr.length; i++) {
  arr[i].dataset.index = i;
  arr[i].addEventListener('click', handleClick);
}

Inline Small Functions #

// Bad - Function call overhead
function square(x) {
  return x * x;
}

const result = arr.map(x => square(x));

// Good - Inlined
const result = arr.map(x => x * x);

Use Rest Parameters Carefully #

// Slower - Creates array
function sum(...args) {
  return args.reduce((a, b) => a + b, 0);
}

// Faster - Fixed parameters
function sum(a, b, c) {
  return a + b + c;
}

DOM Optimization #

Batch DOM Updates #

// Bad - Multiple reflows
for (let i = 0; i < 100; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  document.body.appendChild(div);  // Reflow each time
}

// Good - Single reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  fragment.appendChild(div);
}
document.body.appendChild(fragment);  // Single reflow

Cache DOM Queries #

// Bad
for (let i = 0; i < 100; i++) {
  document.getElementById('container').innerHTML += i;
}

// Good
const container = document.getElementById('container');
for (let i = 0; i < 100; i++) {
  container.innerHTML += i;
}

// Best
const container = document.getElementById('container');
const parts = [];
for (let i = 0; i < 100; i++) {
  parts.push(i);
}
container.innerHTML = parts.join('');

Use Event Delegation #

// Bad - Multiple listeners
const items = document.querySelectorAll('.item');
items.forEach(item => {
  item.addEventListener('click', handleClick);
});

// Good - Single listener
document.querySelector('.container').addEventListener('click', (e) => {
  if (e.target.classList.contains('item')) {
    handleClick(e);
  }
});

Memory Optimization #

Avoid Memory Leaks #

// Bad - Closure keeps reference
function createLeak() {
  const bigArray = new Array(1000000).fill('leak');

  return function() {
    // bigArray is kept in memory
    console.log('leaked');
  };
}

// Good - Release references
function noLeak() {
  let bigArray = new Array(1000000).fill('no leak');

  setTimeout(() => {
    bigArray = null;  // Allow GC
  }, 1000);
}

Clear Event Listeners #

// Bad - Listener keeps reference
const element = document.getElementById('myElement');
element.addEventListener('click', handleClick);

// Good - Remove when done
element.removeEventListener('click', handleClick);

// Best - Use AbortController
const controller = new AbortController();
element.addEventListener('click', handleClick, { signal: controller.signal });

// Later
controller.abort();  // Removes all listeners

WeakMap for Metadata #

// Bad - Prevents GC
const metadata = new Map();

function setMetadata(obj, data) {
  metadata.set(obj, data);  // obj never GC'd
}

// Good - Allows GC
const metadata = new WeakMap();

function setMetadata(obj, data) {
  metadata.set(obj, data);  // obj can be GC'd
}

Async Optimization #

Use Promise.all for Parallel Operations #

// Bad - Sequential
async function fetchAll(urls) {
  const results = [];
  for (const url of urls) {
    results.push(await fetch(url));
  }
  return results;
}

// Good - Parallel
async function fetchAll(urls) {
  return Promise.all(urls.map(url => fetch(url)));
}

Lazy Loading #

// Load modules on demand
async function loadFeature() {
  const module = await import('./feature.js');
  module.init();
}

// Lazy load images
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);
});

Code Splitting #

Dynamic Imports #

// Load code only when needed
button.addEventListener('click', async () => {
  const module = await import('./heavy-module.js');
  module.doSomething();
});

Tree Shaking #

// Named exports enable tree shaking
export function used() { }
export function unused() { }  // Will be removed

// Usage
import { used } from './module.js';  // Only 'used' bundled

Worker Threads #

Web Workers for Heavy Computation #

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

worker.postMessage({ data: largeArray });

worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = processLargeArray(e.data.data);
  self.postMessage(result);
};

function processLargeArray(arr) {
  // Heavy computation
  return arr.map(x => x * 2);
}

Best Practices #

  1. Measure first - Profile before optimizing
  2. Optimize bottlenecks - Focus on hot paths
  3. Use appropriate data structures - Map/Set vs Object/Array
  4. Batch DOM updates - Minimize reflows
  5. Cache computed values - Memoization
  6. Debounce/throttle events - Reduce function calls
  7. Lazy load - Load code on demand
  8. Use Web Workers - Offload heavy computation
  9. Minimize object creation - Reuse when possible
  10. Profile regularly - Performance can regress

Performance optimization requires measuring, identifying bottlenecks, and applying appropriate techniques. Focus on algorithmic improvements first, then micro-optimizations.