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:
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
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().