Why is IntersectionObserver better than scroll listeners for lazy-loading and infinite scroll? Implement an infinite-scroll sentinel.
Understanding async, off-main-thread visibility detection vs. throttled scroll + getBoundingClientRect.
The old way — a scroll listener that calls getBoundingClientRect() on candidate elements to see if they're near the viewport — has two problems: scroll fires very frequently (so you must throttle and still do work often), and getBoundingClientRect forces a synchronous layout each call, so you're doing layout-thrashing work on the hot scroll path. IntersectionObserver solves both: you register elements and the browser tells you ASYNCHRONOUSLY, off the main thread, when their intersection with a root (the viewport or a scroll container) crosses thresholds you specify — no polling, no forced layout, no scroll handler. It's the right tool for: lazy-loading images/components as they approach the viewport, firing analytics when a section becomes visible, pausing offscreen work, and infinite scroll. Key options: `root` (the scroll container, default viewport), `rootMargin` (grow the root's box so you can start loading BEFORE the element is actually visible — e.g., '200px' preloads just off-screen), and `threshold` (fire at 0%, 50%, fully visible, etc.). For infinite scroll the clean pattern is a SENTINEL: an empty element after the last item; when it intersects (with a rootMargin so it triggers a bit early), fetch the next page and append — then the sentinel moves down and re-triggers. This avoids attaching/detaching scroll math and naturally batches. Gotchas: unobserve/disconnect when done to avoid leaks and duplicate fetches; guard against firing while a fetch is already in flight; and note it reports visibility, not pixels-scrolled, so for a scroll-progress bar you still want rAF + scroll. Native `loading=lazy` on <img>/<iframe> covers the image case without any JS, so reserve IntersectionObserver for components, analytics, and pagination.
Lazy-loading components, visibility analytics, or infinite scroll without janky scroll handlers.
No per-scroll work; callbacks fire only on threshold crossings, off the main thread
// Infinite scroll via a sentinel element after the list
const sentinel = document.querySelector('#load-more');
let loading = false;
const io = new IntersectionObserver(async (entries) => {
const entry = entries[0];
if (!entry.isIntersecting || loading) return;
loading = true;
const page = await fetchNextPage(); // append rows
render(page);
loading = false;
if (page.isLast) io.disconnect(); // stop observing when done
}, {
root: null, // viewport
rootMargin: '400px', // start loading before the sentinel is visible
threshold: 0,
});
io.observe(sentinel);