Community JavaScript Snippet
IntersectionObserver Batched With rootMargin
On a feed with 200 cards, creating one IntersectionObserver per card pushed our scroll frame to 14ms. This is the single shared observer with `rootMargin` prefetch and a batched callback that brought it back to 4ms.
IntersectionObserver Batched With rootMargin
On a feed with 200 cards, creating one IntersectionObserver per card pushed our scroll frame to 14ms. This is the single shared observer with `rootMargin` prefetch and a batched callback that brought it back to 4ms.
By @emmakim
April 9, 2026
·
Updated May 18, 2026
500 views
9
Rate
Sharing one observer is the optimization the spec already enables but most code does not use. The browser batches the callback for you (entries arrive as an array per tick), so handling them in groups instead of per-element keeps the scroll frame cheap. The Set of currently-visible elements is what makes the diff possible: without it, the callback fires once on initial observe with isIntersecting: false for every element, and naive code marks them all hidden. The onChange listener pattern means a feed component, an analytics module, and a lazy-image loader can all subscribe to the same observer.
rootMargin accepts CSS-style top/right/bottom/left values; positive values expand the rectangle, negative values shrink it. For lazy-loading I use 200px 0px 200px 0px so we start fetching when the image is roughly one viewport above or below visible, which feels instant on a normal scroll. For analytics impressions I do the opposite: a negative rootMargin of -25% means the impression only counts when the card is fully past the top quarter of the viewport, which suppresses the noisy half-second hover at the fold. Same observer API, two completely different use cases.
An array of thresholds tells the observer to fire whenever intersectionRatio crosses one of the listed values. Five steps is the sweet spot I have landed on for cinematic scroll effects: 21 thresholds (every 5%) is overkill and starts to lag on Safari, while a single threshold means abrupt jumps. The browser automatically sorts and de-duplicates the array, so I do not bother. For a parallax effect I sometimes pair this with rootMargin to extend the callback range past the actual viewport, which keeps the animation playing as the section exits.
