10 JavaScript Traps That Make Your Code Slower (and How to Fix Them)
10 JavaScript Traps That Make Your Code Slower (and How to Fix Them)
We’ve all shipped code that “worked” but felt sluggish. Here are the most common performance traps I still see in real projects—plus simple, copy-pasteable fixes.
By Regadamilli Udbhav ·
Rule of thumb: fix what users feel first. Measure with Performance panel, console.time(), or a tiny A/B before refactoring everything.
1) DOM thrashing: repeated measure → mutate → measure
Measuring layout (like offsetHeight) between mutations forces the browser to flush style & layout repeatedly.
// ❌ Thrashes layout on every iteration
for (const el of items) {
const h = el.offsetHeight; // measure
el.style.height = (h + 10) + "px"; // mutate
}
// ✅ Batch reads, then writes
const heights = [];
for (const el of items) heights.push(el.offsetHeight);
for (let i = 0; i < items.length; i++) items[i].style.height = (heights[i] + 10) + "px";
Also good: mutate via a wrapper class (el.classList.add), use requestAnimationFrame, or build fragments off-DOM.
2) Heavy array method chains in hot paths
Pretty chains create temporary arrays and callbacks. In tight loops, that overhead adds up.
// ❌ Multiple passes + allocations
const total = arr.filter(x => x > 0).map(x => x * 2).reduce((a,b) => a + b, 0);
// ✅ One pass
let total = 0;
for (const x of arr) if (x > 0) total += x * 2;
Use expressive chains for non-critical paths; switch to single-pass where latency matters.
3) N+1 DOM queries & listeners
Querying inside loops or attaching thousands of handlers slows everything down.
// ❌ Querying every click target separately
document.querySelectorAll(".item").forEach(el => {
el.addEventListener("click", () => handle(el.dataset.id));
});
// ✅ Event delegation + one listener
document.addEventListener("click", (e) => {
const el = e.target.closest(".item");
if (!el) return;
handle(el.dataset.id);
});
4) Blocking the main thread with heavy sync work
Large JSON parses, crypto, image processing—done sync—freeze the UI.
// ❌ Big CPU tasks on the main thread
const result = heavyCompute(data); // jank
// ✅ Offload to a Web Worker
// worker.js
self.onmessage = ({ data }) => postMessage(heavyCompute(data));
// main.js
const worker = new Worker("worker.js");
worker.postMessage(data);
worker.onmessage = ({ data }) => render(data);
5) Function churn & garbage collection pressure
Creating new closures in hot paths creates short-lived garbage; GC pauses can cause stutters.
// ❌ New function every tick
setInterval(() => doWork(expensiveObj), 16);
// ✅ Reuse stable references
function tick(){ doWork(expensiveObj); }
setInterval(tick, 16);
Hoist functions, reuse arrays/objects where safe, and avoid capturing giant scopes.
6) Slow deep clones with JSON.parse(JSON.stringify())
It’s lossy (Dates, Maps, functions) and slow on large objects.
// ❌ Slow & lossy
const clone = JSON.parse(JSON.stringify(obj));
// ✅ Faster & more correct where supported
const clone = structuredClone(obj);
Need wider support? Consider a well-tuned library or domain-specific cloning.
7) Framework re-render floods (React/Vue/etc.)
Inline arrow props, missing keys, or broad state updates cause extra renders.
// ❌ New callback each render (React)
<List onSelect={id => setActive(id)} items={items} />
// ✅ Memoize & stable props
const onSelect = useCallback((id) => setActive(id), [setActive]);
<List onSelect={onSelect} items={items} />
Use React.memo, useMemo, proper keys, and split state so updates are surgical.
8) Unthrottled scroll/resize/input handlers
These events can fire dozens of times per frame.
// ❌ Fires 60+ times/sec
window.addEventListener("scroll", onScroll);
// ✅ Throttle + passive listeners
function throttle(fn, wait){
let t = 0;
return (...a) => {
const now = Date.now();
if (now - t >= wait){ t = now; fn(...a); }
};
}
window.addEventListener("scroll", throttle(onScroll, 100), { passive: true });
9) Big bundles, no code splitting
Users pay the cost up front if you ship everything in one JS file.
// ✅ Dynamic import for routes/features
async function loadChart(){
const { Chart } = await import("./charts.js");
new Chart(...);
}
- Enable tree-shaking (ES modules).
- Lazy-load rare pages/components.
- Defer non-critical scripts.
10) Chatty APIs & zero caching
Too many requests + no cache = wasted time and battery.
// ✅ Basic in-memory cache for GETs
const cache = new Map();
async function getJSON(url){
if (cache.has(url)) return cache.get(url);
const res = await fetch(url, { headers: { "Accept": "application/json" }});
const data = await res.json();
cache.set(url, data);
return data;
}
Use HTTP caching (ETag, Cache-Control), paginate responses, and batch or dedupe requests.
Performance Checklist (copy & pin)
- Batch DOM reads/writes; avoid layout thrash.
- Measure hot paths; collapse array chains there.
- Delegate events; cache selectors.
- Move CPU work to Web Workers.
- Stabilize function references; avoid closure bloat.
- Use
structuredClone(or targeted clones). - Control re-renders with memoization & proper keys.
- Throttle/debounce scroll/resize/input; use passive listeners.
- Code-split, lazy-load, and tree-shake.
- Cache aggressively and reduce round trips.
FAQ
How do I know which trap hurts me most?
Open DevTools → Performance. Record user actions; look for long tasks (>50ms), layout thrash, and scripting spikes.
Is micro-optimizing worth it?
Only in hot paths. Fix what users feel first—input latency, scroll, route changes—then iterate.
Comments
Post a Comment