JavaScript performance: keep the main thread free
A page can load fast and still feel slow. Batch DOM updates, debounce noisy events and break up long tasks to keep the interface responsive.
Loading speed gets most of the attention, but users also judge a page by what happens after it loads. They tap a filter and nothing moves for half a second. The cause is nearly always the same: JavaScript is holding the main thread.
The browser uses one thread for your scripts, for layout, for painting and for handling input. While a script runs, a tap has to wait. Any task longer than 50 ms counts as a long task, and Interaction to Next Paint (INP), one of the Core Web Vitals, measures the delay users feel. Under 200 ms is good.
Touch the DOM less
Reading a layout value such as offsetWidth right after changing a style forces the browser to recalculate layout immediately. Inside a loop, that happens on every pass.
// Slow: layout is recalculated on every iteration
for (const item of items) {
item.style.width = container.offsetWidth / 2 + "px";
}
// Fast: read once, then write
const half = container.offsetWidth / 2 + "px";
for (const item of items) {
item.style.width = half;
}
Read first, write second. Keep references to elements you use repeatedly instead of querying for them again, and update only the part of the page that changed.
Debounce noisy events
input, scroll and resize can fire dozens of times a second. Running a search or a filter on each one is wasted work.
function debounce(fn, wait = 200) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
searchBox.addEventListener("input", debounce(runSearch));
Break up long tasks
When a loop has to process thousands of items, give the browser a chance to handle input in between.
async function processAll(items) {
for (const [i, item] of items.entries()) {
process(item);
if (i % 200 === 0) await new Promise((resolve) => setTimeout(resolve));
}
}
The total work is the same. The page stays responsive while it happens.
Render only what is on screen
A list of five thousand product rows does not need five thousand DOM nodes. Paginate, or use a virtual list that renders only the visible rows. For long static pages, the CSS property content-visibility: auto lets the browser skip rendering sections that are off screen.
Move heavy work off the main thread
Parsing a large file, resizing an image or crunching numbers does not have to block the interface at all. Web Workers run that code on a separate thread.
Find the real culprit
Do not guess. Record the slow interaction in the Performance panel and look for the long task behind it. Our guide to finding bottlenecks with Chrome DevTools walks through it.