Quiz

What are some common performance bottlenecks in JavaScript applications?

Topics
JavaScriptPerformance

TL;DR

Common bottlenecks include too much JavaScript during startup, long main-thread tasks, repeated layout work, excessive rendering, slow or duplicated network requests, and memory retained by long-lived references. Do not optimize from a checklist: reproduce the slow user action, record it with browser or Node.js profiling tools, fix the dominant cost, and measure again under the same conditions.


Inefficient DOM manipulation

Frequent DOM updates

Frequent DOM updates can be costly because the browser has to re-render the page each time the DOM changes. Batch DOM updates together to minimize reflows and repaints.

// Inefficient
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
document.body.appendChild(div);
}
// Efficient
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);

Layout thrashing

Layout thrashing occurs when you read and write to the DOM repeatedly, causing multiple reflows and repaints. Minimize layout thrashing by batching reads and writes separately.

// Inefficient
boxes.forEach((box) => {
const height = box.offsetHeight; // Read
box.style.height = `${height + 10}px`; // Write
});
// Efficient
// Batch read
const heights = [];
boxes.forEach((box) => {
heights.push(box.offsetHeight);
});
// Batch write
boxes.forEach((box, i) => {
box.style.height = `${heights[i] + 10}px`;
});

Blocking the main thread

Heavy computations

Heavy computations can block the main thread, making the UI unresponsive. Use web workers to offload heavy computations to a background thread.

// Main thread
const worker = new Worker('worker.js');
worker.postMessage('start');
// worker.js
self.onmessage = function (e) {
if (e.data === 'start') {
// Perform heavy computation
self.postMessage('done');
}
};

Moving work to a worker is worthwhile only when the computation is large enough to justify startup and message-transfer costs. Smaller work may be better split into chunks so the browser can process input and render between them.

Memory leaks

Memory leaks occur when reachable references retain data the application no longer needs. Circular references alone are not leaks in modern tracing garbage collectors. Common causes include unbounded caches, forgotten timers, detached DOM nodes, observers, and listeners attached to long-lived targets.

Unremoved event listeners

const controller = new AbortController();
window.addEventListener('resize', handleResize, {
signal: controller.signal,
});
// When this feature is disposed:
controller.abort();

Improper use of asynchronous operations

Unoptimized promises

Promise chains and async/await are equivalent ways to express the same asynchronous control flow. Performance suffers when independent operations are awaited one after another; start them together when their results do not depend on each other.

// Inefficient: independent requests run sequentially.
async function fetchDashboardDataSequentially() {
const user = await fetch('/api/user').then((response) => response.json());
const notifications = await fetch('/api/notifications').then((response) =>
response.json(),
);
return { user, notifications };
}
// Efficient: independent requests run concurrently.
async function fetchDashboardDataConcurrently() {
const [user, notifications] = await Promise.all([
fetch('/api/user').then((response) => response.json()),
fetch('/api/notifications').then((response) => response.json()),
]);
return { user, notifications };
}

Debouncing and throttling

Use debouncing and throttling to limit the rate of function execution, especially for event handlers.

// Debouncing
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Throttling
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Users report that opening a dashboard is slow and sometimes janky. How would you find the dominant cause and verify a fix?