Skip to content
JavaScript best practices 3 min read

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));
TechniqueRunsBest for
DebounceAfter activity stopsSearch input, autosave, validation
ThrottleAt a fixed rateScroll 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 requestAnimationFrame for visual updates instead of timers.
  • Lazy load code with dynamic import() and content with IntersectionObserver.
  • Choose Set/Map for membership and lookups, and memoize expensive pure functions.
  • Offload heavy computation to Web Workers and keep bundles small with tree-shaking and code-splitting.
Last updated June 1, 2026
Was this helpful?