How do you detect when an element enters the viewport without janky scroll listeners? Implement lazy-loading with IntersectionObserver.
Whether you replace scroll-handler + getBoundingClientRect polling with the async observer.
IntersectionObserver asynchronously notifies you when a target element's visibility relative to a root (the viewport by default) crosses configured thresholds — WITHOUT running code on every scroll event. The old approach — a scroll listener calling getBoundingClientRect on each element — fires constantly, forces synchronous layout, and janks the main thread. The observer instead batches these checks off the main thread and calls your callback only when a threshold is crossed. API: new IntersectionObserver(callback, { root, rootMargin, threshold }); call observer.observe(el) per target. threshold (0–1, or an array) sets how much must be visible to fire (0 = any pixel, 1 = fully visible). rootMargin grows/shrinks the root box — e.g. '200px' fires 200px BEFORE the element scrolls in, perfect for pre-loading. In the callback, each entry has isIntersecting, intersectionRatio, and target; typically you act then observer.unobserve(entry.target) so it fires once. Classic uses: lazy-loading images (swap data-src → src when near viewport — though native loading='lazy' now covers the simple case), infinite scroll (observe a sentinel at the list bottom and fetch the next page), and firing analytics/animations when a section appears. Sibling APIs: ResizeObserver (size changes) and MutationObserver (DOM changes).
Lazy-loading, infinite scroll (sentinel), and viewport-triggered animation/analytics — instead of scroll polling.
// (Sandboxed here: needs real scrollable layout, so this is read-only reference.)
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // swap in the real image when near viewport
io.unobserve(img); // load once, then stop watching
}
}
}, {
root: null, // viewport
rootMargin: '200px', // start loading 200px BEFORE it scrolls in
threshold: 0
});
document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));