Performance Tips
The fastest code is the code that never runs, and the second fastest is the code that runs once instead of a thousand times. Most JavaScript performance problems come from touching the DOM too often, doing work on every scroll or keystroke, and loading everything up front. The techniques below are practical and high-impact — and the most important habit of all is to measure first, because intuition about what is slow is frequently wrong.
Measure before optimizing
Guessing wastes effort. Profile with the browser’s Performance panel, mark hot paths with performance.now(), and confirm a change actually helped before keeping it.
const start = performance.now();
doExpensiveWork();
console.log(`took ${(performance.now() - start).toFixed(2)}ms`);
Optimize the bottleneck you measured, not the one you imagined. A 5% slice of total time is rarely worth complicating your code for.
Minimize DOM access and reflows
The DOM is the slowest thing most apps touch. Reading a layout property (offsetHeight, getBoundingClientRect) right after writing one forces a synchronous reflow — interleaving them in a loop causes “layout thrashing.”
// Slow: read/write interleaved → reflow every iteration
items.forEach((el) => { el.style.width = el.offsetWidth + 10 + "px"; });
// Fast: batch reads, then batch writes
const widths = items.map((el) => el.offsetWidth);
items.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });
Build large DOM subtrees off-screen with a DocumentFragment and insert them in one operation, and cache element references instead of re-querying the same node.
const fragment = document.createDocumentFragment();
for (const item of data) {
const li = document.createElement("li");
li.textContent = item.name;
fragment.append(li);
}
list.append(fragment); // single DOM insertion
Debounce and throttle high-frequency events
Events like input, scroll, resize, and mousemove fire dozens of times per second. Limit how often your handler actually runs.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
searchBox.addEventListener("input", debounce((e) => search(e.target.value), 300));
| Technique | Runs | Best for |
|---|---|---|
| Debounce | After activity stops | Search input, autosave, validation |
| Throttle | At a fixed rate | Scroll position, drag, resize |
Use requestAnimationFrame for visual updates
Drive animations and scroll-linked visual changes with requestAnimationFrame so updates align with the browser’s paint cycle (≈60fps) instead of running too often or stuttering.
function update() {
box.style.transform = `translateX(${pos}px)`;
pos += 2;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Lazy load what you do not need yet
Don’t pay for code or assets before they’re used. Split rarely-used code behind dynamic import(), and reveal off-screen content with IntersectionObserver instead of loading everything up front.
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js"); // loaded on demand
renderChart(data);
});
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src; // load image when visible
observer.unobserve(entry.target);
}
}
});
document.querySelectorAll("img[data-src]").forEach((img) => observer.observe(img));
Avoid unnecessary work and allocations
Hoist invariant computations out of loops, cache repeated lookups, and use the right data structure. A Set or Map turns repeated O(n) Array.includes checks into O(1).
// Slow: O(n) lookup inside an O(n) loop → O(n²)
const exists = (id) => bigArray.includes(id);
// Fast: O(1) lookups
const idSet = new Set(bigArray);
const exists2 = (id) => idSet.has(id);
Cache pure-function results with memoization, and short-circuit early so you skip work whose result you already know.
Offload heavy computation to a Web Worker
Long synchronous tasks block the main thread and freeze the UI. Move CPU-heavy work (parsing, image processing, large loops) into a Web Worker so the page stays responsive.
const worker = new Worker("./heavy.js");
worker.postMessage(largeDataset);
worker.onmessage = (e) => render(e.result);
Keep bundles lean
Smaller payloads parse and execute faster. Rely on tree-shaking by using named ES module imports, code-split routes and features, and audit dependencies before adding them — a date or utility library is often heavier than the few functions you need.
Best Practices
- Profile and measure first; optimize the proven bottleneck, not a guess.
- Batch DOM reads then writes, and build subtrees with a
DocumentFragment. - Debounce input-driven work and throttle high-frequency events.
- Use
requestAnimationFramefor visual updates instead of timers. - Lazy load code with dynamic
import()and content withIntersectionObserver. - Choose
Set/Mapfor membership and lookups, and memoize expensive pure functions. - Offload heavy computation to Web Workers and keep bundles small with tree-shaking and code-splitting.