IntersectionObserver powers lazy-loaded images, infinite scroll, and "viewed" analytics by telling you when an element enters or leaves the viewport — efficiently, off the main thread. On older browsers you fake it the old-fashioned way: listen to scroll and resize, measure each watched element with getBoundingClientRect(), and fire when its on-screen status flips. Building that fallback shows exactly what the native API hides.
Implement intersectionObserverPolyfill(callback, options) returning an observer with observe, unobserve, and disconnect, matching the real API's shape. See MDN: IntersectionObserver.
function intersectionObserverPolyfill(callback, options) {
return { observe, unobserve, disconnect };
}
// callback(entries, observer); entry =
// { target, isIntersecting, intersectionRatio, boundingClientRect }
const io = intersectionObserverPolyfill((entries) => {
for (const e of entries) if (e.isIntersecting) load(e.target);
});
io.observe(image); // fires an initial entry immediately
// ...user scrolls; when `image` enters the viewport, the callback runs again
io.disconnect(); // detaches scroll/resize listeners
(0, 0, window.innerWidth, window.innerHeight); an element intersects when its rect overlaps it.isIntersecting; only report the ones that flipped on a given scroll/resize.observe(el) delivers one entry right away with the element's current state, like the native observer.intersectionRatio — the fraction of the element's area that's visible (0 when off-screen, 1 when fully in view). Clean up listeners in disconnect.