The Dance Of JavaScript In the Browser
High-level concepts you must understand.
- Useful patterns & examples
- Common pitfalls & gotchas
- Testing & determinism
- Performance & battery considerations
High-level concepts you must understand.
- Event loop phases / task queues
- While the event loop itself isn’t directly DOM-related, many tasks scheduled by the event loop can be. For instance:
- *Microtask(like
.thenof aPromise) can modify the DOM (e.g., updating elements one aPromiseresolves). - Macrotasks (like
setTimeout,setInterval, orrequestAnimationFrame) can also interact with the DOM by manipulating elements, triggering events, or performing layout/repaint tasks. -
setTimeout/setIntervalcallbacks are scheduled on the macrotask (task) queue (in browsers the “timer” phase). Microtasks (Promises.then,queueMicrotask) run before macrotasks that follow the currently executing code. - Consequence: resloving a promise can run before a
setTimeout(..., 0)handler.
- *Microtask(like
- While the event loop itself isn’t directly DOM-related, many tasks scheduled by the event loop can be. For instance:
- Zero-delay is not immediate
-
setTimeout(fn, 0)schedulesfnto run after current code and any queued microtasks - it is yielding control, not instantaneously.
-
- setTimeout / setInterval Callbacks:
- These are typically used for scheduling DOM updates, such as animations or polling for state changes, so they are often DOM-related when used in client-side JavaScript.
- For example, you might use
setTimeoutto delay DOM updates after a certain action, orsetIntervalto repeatedly update an element at a fixed interval.
- Clamping and throttling
- These can impact the performance of DOM updates. If your’re using high-frequency timers to update the DOM, browsers may throttle them when tabs are inactive or when there are too many nested timers. This is particularly important in animations or continous DOM updates.
- Browsers clamp nested timers or timers in background/inactive tabs. Historically nested timeouts < 4ms get clamped to 4ms; background tabs may be throttled to ~100ms or more. Node has different rules.
- Accuracy and Drift
- Timers that drift can cause issues with timing-sensitive DOM updates (like animations or periodic UI updates). For example, if you rely on
setIntervalto update an animation, any drift can result in visible jank or inconsistent behavior in the UI.
- Timers that drift can cause issues with timing-sensitive DOM updates (like animations or periodic UI updates). For example, if you rely on
- Timers and Async Functions
-
If you pass asn async function to a
setIntervalorsetTimeout, it won’t behave as expected, since the function will run asynchronously. This could lead to race conditions or unexpected behavior when manipulating the DOM.For example, if the async function updates the DOM, it could result in multiple concurrent updates that might not be desirable.
-
- Memory Leaks and Closures
- This is very relevant for DOM manipulation, as closures (functions referencing DOM nodes) can prevent garbage collection if not cleared properly. If you don’t clear your timers (especially if they are tired to DOM elements), you might create memory leaks that accumulate over time as the DOM grows.
- Security / DoS / Throttlingh
- While not directly manipulating the DOM, this is relevant to DOM performance. Excessive timers can throttle DOM updates or cause a page to become unresponsive ( leading to poor user experience ). For instance, long-running intervals or timeouts that update the DOM can overwhelm the browser’s rendering enginer,, causing frames to drop or UI freezes.
Useful patterns & examples
-
Promise sleep /
awaitfriendlyfunction sleep(ms, {signal} = {}){ return new Promise((resolve, reject) => { if(signal?.aborted) { return reject(new DOMException('Aborted', 'AbortError')); } const id = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms); function onAbort() { clearTimeout(id); reject(new DOMExecption('Aborted', 'AbortError')); } signal?.addEventListenter('abort', onAbort, {once: true}); }); }This is better than ad-hoc setTimeouts when you use
async/awaitand want cancellation support. -
Accurate interval (drift-compensated)
function startAccurateInterval(fn, interval) { let expected = performance.now() + interval; let stopped = false; function tick() { if (stopped) { return; } fn(); expected += interval; const next = Math.max(0, interval - drift); setTimeout(tick, next); } setTimeout(teck, interval); return () => { stopped = true; }; } //usage const stop = startAccurateInterval(() => console.log('tick', performance.now()), 1000); // stop() to cancelThis corrects for drift caused by callback execution time.
-
Avoid overlapping
asyncinterval runlet running = false; const id = setInterval(async () => { if (running) return; running = true; try { await doSomethingAsync(); } finally { running = false; } }, 1000)Or use recursive
setTimeoutto enforce sequential runs. -
Exponential backoff + jitter (for retries)
function backoff(attemp, base = 200, cap = 10000){ const exp = Math.min(cp, base * (2 ** attemp)); // add jitter return exp / 2 + Math.random() * (exp / 2) }Use
setTimeoutwith the returned ms. Jittering avoids thundering herd. -
Cancelable timers with AbortController
function setTimeoutWithSignal(cb, ms, signal) { const id = setTimeout(() => { signal?.removeEvenListener('abort', onAbort); cb(); },ms) function onAbort(){ clearTimeout(id); } signal?.addEventListener('abort', onAbort, {once: true}); return id; } const ac = new AbortController() { setTimeoutWithSignal(() => console.log('done'), 5000, ac.signal); ac.abort(); // cancels } - Use
requestAnimationFramefor animations.-
requestAnimationFrameis synchronized with display refresh rate and pauses in background tabs - use it instead ofsetIntervalfor visual updates.
-
-
Yiedling to the event loop (cooperative blocking) If you must do heavy sychronous work but want to keep UI responsive, break it into slices:
async function bigWork(items) { for (let i = 0; i<items.length; i++) { process(items[i]); if(i % 100 === 0){ await new Promise(r => setTimeout(r, 0)); // yield } } }
Common pitfalls & gotchas
- Relying on exact timing - browsers and OSes will vary. Never assume millisecond-perfect scheduling for non-real-time tasks.
- setInterval + long task overlap - causes multiple simultaneous executions; use locking or recursive scheduling.
- Timers keep objects alive - forgettiing to clear timers tied to DOM nodes leads to leaks.
- Using
setIntervalfor animations - leads to frame-skip/jitter; preferrequestAnimationFrame. - Nested setTimeout clamping - repeatedly calling ~setTimeout(…, 0) inside handlers can get clamped to a minimum delay.
- In tests - use fake timers(Jest, Sinon) for deterministic behavior. Be aware fake timers change how
Date.now()andperformance.now()behave in some libs.
Testing & determinism
- Use Sinon or Jest fake timers to:
- Advance time deterministically.
- Test backoff and retry logic.
- Avoid flakiness in asynchronours tests.
- But beware: some APIs (like
requestAnimationFrame, or high-resperformace.now()) need additional shims or cannot be faked the same way.
Performance & battery considerations
- Avoid frequent timers on background tabs; check
document.visibilityStateand pause timers when hidden. - For background processing use Web Workers or Service Workers where appropriate (and the browser allows longer lifecycle).