IntersectionObserver — Lost Sentinel Breaks Infinite Scroll
When a sentinel is removed from the DOM, IntersectionObserver stops firing - breaking infinite scroll.
- IntersectionObserver asynchronously notifies when a target element enters/exits the viewport or a container
- Create an observer with a callback and options, then call observer.observe(element)
- Callback receives an array of IntersectionObserverEntry objects with isIntersecting, intersectionRatio, target
- rootMargin extends the viewport – positive values fire earlier, negative later
- threshold array triggers callbacks at multiple visibility levels (e.g., [0, 0.25, 0.5, 0.75, 1])
- Always call unobserve() when done to avoid memory leaks and unnecessary computations
IntersectionObserver is a browser API that tells you when an element scrolls into view (or out of view). Instead of constantly checking scroll position yourself, the browser does the hard work and calls your code only when visibility changes. Great for lazy loading images, infinite scrolling, and triggering animations when elements appear on screen.
Basic Usage — Lazy Loading Images
The most common use case for IntersectionObserver is lazy loading images. Instead of loading all images on page load, you load a placeholder and replace the src when the image scrolls into view. This reduces initial page weight and speeds up perceived performance.
The observer is created with a callback that fires whenever an observed element's visibility changes. Inside the callback, check entry.isIntersecting – when true, swap the real image URL from a data-src attribute, add a class, then unobserve the element to stop watching it.
threshold and rootMargin Options
The threshold option defines at what percentage of the target's visibility the callback fires. A single value like 0.5 fires when exactly half the element is visible (in either direction). An array [0, 0.25, 0.5, 0.75, 1] fires five times per crossing. Use this for animations that need to know overlap progress.
rootMargin expands (positive px) or shrinks (negative px) the root's bounding box before checking intersection. A positive rootMargin makes the observer fire earlier; negative delays the fire. This is useful for pre-loading content or avoiding action until the element is comfortably inside the viewport.
Infinite Scroll
Infinite scroll uses a sentinel element placed after the content list. When the sentinel becomes visible, you fetch the next page of data and append it. IntersectionObserver on the sentinel fires the fetch logic when the user scrolls near the bottom.
Critical details: use a loading guard (isLoading) to avoid duplicate fetches, and unobserve the sentinel when there is no more data (to stop unnecessary calls). The rootMargin should be positive (e.g., 300px) to start loading before the user hits the bottom, giving the network request time to complete.
Performance and Memory Management
Each IntersectionObserver instance uses a small amount of system resources. When observing many elements (hundreds or thousands), you risk main thread overhead from the observer's internal checks. Use a single observer for many elements rather than creating per-element observers. The browser handles the intersection calculations efficiently as long as the number of observed elements stays reasonable (< 1000).
Memory leaks occur when you forget to call unobserve() or disconnect(). Observed elements that are removed from the DOM without being unobserve'd hold references that prevent garbage collection. This is especially problematic in SPAs where components mount and unmount repeatedly.
Using IntersectionObserver with Frameworks (React, Vue, Angular)
In component-based frameworks, you typically create an IntersectionObserver inside a component's lifecycle (useEffect in React, onMounted in Vue, ngAfterViewInit in Angular) and disconnect it when the component unmounts. Use refs to get a reference to the DOM element to observe.
For lazy loading images, you can encapsulate the logic in a custom hook (React) or a directive (Angular). The key is to ensure the observer is created after the element is rendered and cleaned up when the element is removed.
Infinite Scroll Stops Loading After Reaching a Hidden Sentinel
observer.unobserve() removes the observer. Use a rootMargin of 300px so the sentinel can be positioned absolutely below the list, never removed on re-render.- IntersectionObserver stops firing if the target element is removed from the DOM.
- Always keep sentinel elements outside the rendering logic that might detach them.
- Test infinite scroll with partial data and verify observer fires after multiple loads.
Observer.unobserve() targets after they finish (e.g., after image loaded). Do not observe more than ~100 elements per observer; split into multiple observers if needed.Key takeaways
observe() on many targets.Common mistakes to avoid
5 patternsForgetting to unobserve after element is processed
Using threshold [0, 1] without understanding direction
Not accounting for rootMargin units
Creating a new observer for each element in a loop
observe() on each target element. The observer's callback receives an array of entries for that single observer. Reuse it.Assuming IntersectionObserver works inside iframes
Interview Questions on This Topic
How would you implement lazy loading of images without IntersectionObserver?
Frequently Asked Questions
That's DOM. Mark it forged?
3 min read · try the examples if you haven't