perf hacks

JavaScript Performance

Optimize JavaScript execution for faster, more responsive applications

JavaScript performance directly impacts INP and overall responsiveness. This guide covers techniques to write faster, more efficient JavaScript.

Understanding Main Thread Blocking

The browser's main thread handles:

  • JavaScript execution
  • Layout calculations
  • Paint operations
  • User input processing

Long-running JavaScript blocks all of these:

MAIN THREAD BLOCKINGJavaScript Task (200ms)LayoutPaintInputUser clicks hereMust wait for JS to finish!Blocking main thread

Breaking Up Long Tasks

Using requestIdleCallback

function processLargeArray(items: Item[]) {
  const CHUNK_SIZE = 100;
  let index = 0;

  function processChunk(deadline: IdleDeadline) {
    while (index < items.length && deadline.timeRemaining() > 0) {
      processItem(items[index]);
      index++;
    }

    if (index < items.length) {
      requestIdleCallback(processChunk);
    }
  }

  requestIdleCallback(processChunk);
}

Using setTimeout for Yielding

async function processWithYielding(items: Item[]) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);

    // Yield every 50ms
    if (i % 100 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}

Using scheduler.yield() (Modern)

async function processModern(items: Item[]) {
  for (const item of items) {
    processItem(item);

    // Modern yielding API
    if ('scheduler' in globalThis) {
      await scheduler.yield();
    }
  }
}

Debouncing and Throttling

Debounce for Input Events

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Usage
const handleSearch = debounce((query: string) => {
  searchAPI(query);
}, 300);

<input onChange={(e) => handleSearch(e.target.value)} />

Throttle for Scroll/Resize

function throttle<T extends (...args: any[]) => any>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;

  return (...args: Parameters<T>) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Usage
const handleScroll = throttle(() => {
  updateScrollPosition();
}, 100);

Avoiding Layout Thrashing

The Problem

// ❌ Bad: Forces layout on every iteration
elements.forEach(el => {
  const height = el.offsetHeight;
  el.style.width = height + 'px';
});

The Solution

// ✅ Good: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight);

elements.forEach((el, i) => {
  el.style.width = heights[i] + 'px';
});

Using requestAnimationFrame

function batchDOMUpdates(updates: (() => void)[]) {
  requestAnimationFrame(() => {
    updates.forEach(update => update());
  });
}

// Usage
batchDOMUpdates([
  () => element1.style.transform = 'translateX(10px)',
  () => element2.style.opacity = '0.5',
  () => element3.classList.add('active'),
]);

Optimizing Loops

Fast Array Iteration

// ❌ Slow: forEach with function call overhead
items.forEach(item => processItem(item));

// ✅ Faster: for...of
for (const item of items) {
  processItem(item);
}

// ✅ Fastest: traditional for loop
for (let i = 0; i < items.length; i++) {
  processItem(items[i]);
}

Early Exit Patterns

// ❌ Bad: Checks all items
const found = items.filter(item => item.id === targetId)[0];

// ✅ Good: Stops at first match
const found = items.find(item => item.id === targetId);

// ✅ Good: Check existence
const exists = items.some(item => item.id === targetId);

Avoid Creating Functions in Loops

// ❌ Bad: Creates new function each iteration
items.forEach(item => {
  element.addEventListener('click', () => handleClick(item));
});

// ✅ Good: Use data attributes
items.forEach(item => {
  element.dataset.itemId = item.id;
});

container.addEventListener('click', (e) => {
  const itemId = e.target.dataset.itemId;
  if (itemId) handleClick(itemId);
});

Web Workers for Heavy Computation

worker.ts
self.onmessage = (e: MessageEvent) => {
  const { data, operation } = e.data;

  let result;
  switch (operation) {
    case 'sort':
      result = data.sort((a, b) => a.value - b.value);
      break;
    case 'filter':
      result = data.filter(item => item.active);
      break;
    case 'calculate':
      result = heavyCalculation(data);
      break;
  }

  self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));

function processInWorker(data: any[], operation: string): Promise<any[]> {
  return new Promise((resolve) => {
    worker.onmessage = (e) => resolve(e.data);
    worker.postMessage({ data, operation });
  });
}

// Usage
const sorted = await processInWorker(items, 'sort');

Caching Expensive Operations

Memoization

function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map();

  return ((...args: Parameters<T>) => {
    const key = JSON.stringify(args);

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

    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

// Usage
const expensiveCalculation = memoize((a: number, b: number) => {
  // Heavy computation
  return a ** b;
});

Caching DOM Queries

// ❌ Bad: Queries DOM every time
function updateElement() {
  document.querySelector('.my-element').style.color = 'red';
}

// ✅ Good: Cache the reference
const element = document.querySelector('.my-element');

function updateElement() {
  element.style.color = 'red';
}

Efficient Data Structures

Use Map for Key-Value Lookups

// ❌ Slow: O(n) lookup
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
const user = users.find(u => u.id === targetId);

// ✅ Fast: O(1) lookup
const userMap = new Map(users.map(u => [u.id, u]));
const user = userMap.get(targetId);

Use Set for Uniqueness Checks

// ❌ Slow: O(n) for includes
const seen: string[] = [];
if (!seen.includes(item)) {
  seen.push(item);
}

// ✅ Fast: O(1) for has
const seen = new Set<string>();
if (!seen.has(item)) {
  seen.add(item);
}

Performance Profiling

// Mark performance points
performance.mark('operation-start');

expensiveOperation();

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

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

Checklist

Task Management

Break long tasks into chunks Process data in batches to avoid blocking the main thread.

Use requestIdleCallback for non-urgent work Schedule work when the browser is idle.

Yield to main thread regularly Use setTimeout(0) or scheduler.yield() to allow input processing.

Keep tasks under 50ms Long tasks directly impact INP score.

Event Handling

Debounce input handlers Wait for user to stop typing before processing.

Throttle scroll/resize handlers Limit how often handlers can fire.

Use passive event listeners Add { passive: true } for scroll and touch events.

Remove unused event listeners Clean up listeners when components unmount.

DOM Operations

Batch DOM reads and writes Read all values first, then write all changes.

Cache DOM references Query once and store the reference.

Use requestAnimationFrame Synchronize DOM changes with the browser's paint cycle.

Minimize reflows/repaints Avoid reading layout properties after writes.

Data Processing

Use Web Workers for heavy computation Offload sorting, filtering, and calculations.

Memoize expensive functions Cache results of pure functions.

Choose efficient data structures Use Map and Set for O(1) lookups.

Implement early exit patterns Use find() and some() instead of filter().

On this page