5 More JavaScript Performance Gotchas

Originally published April 2023, follow-up to Part 1. Modern V8 / SpiderMonkey / JavaScriptCore have made parts of this advice obsolete — the entries below are updated for 2026. Same headline rule: measure first; outside a tight, hot loop, almost everything else is noise.

Inefficient use of data structures

Choosing the right data structure has a real impact when collections grow. The classic example: using an array instead of a Set for uniqueness gives you O(n) lookups instead of O(1):

1// Using an array to store unique values 2const uniqueValuesArray = [] 3if (!uniqueValuesArray.includes(value)) { 4 uniqueValuesArray.push(value) 5} 6 7// Using a Set to store unique values 8const uniqueValuesSet = new Set() 9uniqueValuesSet.add(value)

Verdict: for tiny collections (under ~50 entries) the difference is noise — V8's Array.prototype.includes is well-optimized. Past that, Set and Map win measurably. Pick by intent (uniqueness, keyed lookup) first; perf falls out of that.

Not optimizing DOM manipulations

Touching the DOM in a loop forces work the browser would otherwise batch. Most apps use a framework that handles this — but when you're writing vanilla JS, the batching is on you:

1// Appending one element at a time 2const list = document.getElementById('list') 3data.forEach(item => { 4 const li = document.createElement('li') 5 li.textContent = item 6 list.appendChild(li) 7}) 8 9// Batch with append(...) — or DocumentFragment for the same effect 10list.append(...data.map(item => { 11 const li = document.createElement('li') 12 li.textContent = item 13 return li 14}))

Verdict: modern browsers batch synchronous appendChild calls within a task, so the worst case isn't quite what it used to be. append(...nodes) and DocumentFragment are still the cleanest patterns for building a list once. For animation-rate updates, use requestAnimationFrame. Signals-based frameworks (Solid, Svelte 5, Vue Vapor) skip the virtual-DOM diff entirely.

Blocking the event loop

CPU-intensive synchronous code on the main thread blocks rendering and input. Move it to a Web Worker:

1// Blocking the main thread 2function processData(data) { 3 // CPU-intensive task 4} 5 6// Offload to a Web Worker 7const worker = new Worker('worker.js') 8worker.postMessage(data) 9worker.onmessage = (event) => { 10 const result = event.data 11}

Verdict: postMessage deep-clones its argument via the structured-clone algorithm, which can dominate cost for large payloads. Use Transferables (ArrayBuffer, MessagePort, OffscreenCanvas) or SharedArrayBuffer to skip the copy. For shorter yields on the main thread, scheduler.yield() and isInputPending() give finer control than splitting work into setTimeout chunks.

Inefficient event handling

Attaching one listener per element costs memory and re-attachment work for dynamic content. Delegate to a common parent and check event.target:

1// Adding event listeners to individual elements 2const buttons = document.querySelectorAll('button') 3buttons.forEach(button => { 4 button.addEventListener('click', event => { 5 // Handle the click event 6 }) 7}) 8 9// Using event delegation 10const container = document.getElementById('container') 11container.addEventListener('click', event => { 12 if (event.target.closest('button')) { 13 // Handle the click event 14 } 15})

Verdict: event delegation still wins on memory and survives dynamically-inserted children automatically. Prefer event.target.closest('button') over a tagName check so that nested elements (icons, spans inside the button) still match.

Inefficient regular expressions

Catastrophic backtracking happens when nested quantifiers like (a+)+ let the engine try exponentially many ways to match a non-matching input. A non-capturing group is not a fix — /(?:a+)+b/ backtracks exactly as badly as /(a+)+b/:

1// Bad — catastrophic backtracking on "aaaaaaaaaaaaaaaaaaaaaaaaaaaaac" 2const bad = /(a+)+b/ 3 4// Good — flatten the nested quantifier 5const flat = /a+b/

Verdict: flatten nested quantifiers when you can. When you can't (matching genuinely nested structures), reach for atomic groups / possessive quantifiers if your engine supports them, or use a parser instead of a regex. Skip the \d vs [0-9] micro-optimization — modern regex engines compile both to identical bytecode.