webdev.complete
🪟 Browser APIs You Should Know
JavaScript
Lesson 37 of 117
25 min

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

js
// 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));
setInterval can stack up
If your callback takes longer than the interval, the browser doesn't skip ticks. They queue. For long jobs, chain 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.

js
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.

js
requestIdleCallback(deadline => {
  while (deadline.timeRemaining() > 0 && tasks.length) {
    runOne(tasks.shift());
  }
}, { timeout: 2000 });   // guaranteed to fire within 2s

IntersectionObserver: 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:

js
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

js
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

js
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.

// Build 20 cards
const container = document.getElementById("cards");
for (let i = 1; i <= 20; i++) {
  const card = document.createElement("div");
  card.className = "card";
  card.textContent = "Card " + i;
  container.appendChild(card);
}

// Watch each one
const io = new IntersectionObserver(entries => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      entry.target.classList.add("visible");
      io.unobserve(entry.target);
      console.log("revealed:", entry.target.textContent);
    }
  }
}, {
  threshold: 0.2,   // 20% visible triggers the reveal
});

document.querySelectorAll(".card").forEach(el => io.observe(el));

// Bonus: ResizeObserver on the container, log new width
const ro = new ResizeObserver(entries => {
  for (const e of entries) {
    console.log("container width:", Math.round(e.contentRect.width));
  }
});
ro.observe(container);

Quiz

Quiz1 / 3

Which API should you use to animate a moving element smoothly?

Recap

  • Timers: setTimeout for once, recursive setTimeout for safer intervals.
  • requestAnimationFrame for visual animation only. requestIdleCallback for non-urgent work.
  • IntersectionObserver for visibility. The right tool for lazy-loading and reveal animations.
  • ResizeObserver beats listening to window.resize.
  • MutationObserveris a last resort, but invaluable for hooking into DOM you don't control.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.