Observers, Timers, rAF
IntersectionObserver, ResizeObserver, requestAnimationFrame.
Plenty of UI work boils down to "do something later" or "tell me when X changes." Browsers ship a small toolbox of APIs for that: classic timers, a dedicated animation frame callback, idle-time callbacks, and three Observer APIs that replaced thousands of janky polling loops. You'll use one of these almost every day.
setTimeout and setInterval
// setTimeout: run once after a delay
const id = setTimeout(() => console.log("hi"), 500);
clearTimeout(id); // cancel before it fires
// setInterval: run repeatedly every N ms
const tick = setInterval(() => console.log("tick"), 1000);
clearInterval(tick);
// Promise-friendly sleep helper
const sleep = ms => new Promise(r => setTimeout(r, ms));setTimeout inside the handler so each delay starts after the previous run finishes.requestAnimationFrame: animate the right way
For visual animation, never use setInterval. Use requestAnimationFrame(cb): the browser calls your callback just before the next repaint (typically 60 times per second), and pauses the loop when the tab is hidden.
let x = 0;
const box = document.querySelector(".box");
function step(time) {
x = (x + 2) % 300;
box.style.transform = "translateX(" + x + "px)";
requestAnimationFrame(step); // schedule the next frame
}
requestAnimationFrame(step);requestIdleCallback: do it when you can
For non-urgent work (analytics, prefetching, pre-rendering), requestIdleCallback(cb)calls you when the browser has spare time. Don't use it for animation or anything the user is waiting on.
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0 && tasks.length) {
runOne(tasks.shift());
}
}, { timeout: 2000 }); // guaranteed to fire within 2sIntersectionObserver: visibility on demand
"Tell me when this element scrolls into view" used to require listening to scroll and doing math. IntersectionObserver does it for you efficiently:
const io = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("in-view");
io.unobserve(entry.target); // only once
}
}
}, { threshold: 0.1 }); // fire when 10% visible
document.querySelectorAll(".lazy").forEach(el => io.observe(el));Common uses: lazy-loading images, infinite scroll, fade-in animations, "mark as read" when a comment is visible.
ResizeObserver: react to element size
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log("new size:", width, height);
}
});
ro.observe(document.querySelector(".chart"));Way better than listening to window.resize and re-measuring. It fires for any size change, including from layout, not just the viewport.
MutationObserver: watch DOM changes
const mo = new MutationObserver(records => {
for (const r of records) {
if (r.type === "childList") {
console.log("added:", r.addedNodes);
}
}
});
mo.observe(document.body, {
childList: true, // additions/removals
subtree: true, // anywhere in the tree
attributes: false,
});
// Stop watching:
mo.disconnect();Use sparingly: it fires a lot. Useful for hooking into third-party widgets or syncing state with content you don't own.
The performance trio
If you remember nothing else, remember which to reach for:
- Animation:
requestAnimationFrame - Lazy work:
requestIdleCallback - Visibility:
IntersectionObserver - Size changes:
ResizeObserver - DOM changes:
MutationObserver - Delay once:
setTimeout
Try it: fade-in on scroll
The playground below has a column of cards. Each one is hidden until it scrolls into view, then fades in. The handler runs once per card thanks to unobserve.
Quiz
Which API should you use to animate a moving element smoothly?
Recap
- Timers:
setTimeoutfor once, recursive setTimeout for safer intervals. requestAnimationFramefor visual animation only.requestIdleCallbackfor non-urgent work.IntersectionObserverfor visibility. The right tool for lazy-loading and reveal animations.ResizeObserverbeats listening towindow.resize.MutationObserveris a last resort, but invaluable for hooking into DOM you don't control.