I am running into a problem where my intersection observer has two entries both of whose targets are identical but their intersectionRatio is different at the same time in the app. Why could this be? Since it’s the same element and the same viewport, shouldn’t the intersectionRatio be the same? I am scrolling continuously in the app using .scrollTo()
. Is it possible that the element is temporarily removed from the DOM and put back in which is resulting in the two entries?
I am using the IntersectionObserver as:
const intersectionObserverOptions = {
root: rootRef.current,
rootMargin: '0px',
threshold: 0.01
};
const observer = new IntersectionObserver(
intersectionObserverCallback,
intersectionObserverOptions
);
observer.observe(myElement);
2
Answers
When using the IntersectionObserver API, it is possible for two entries for the same element to have different
intersectionRatio
values if the observations were recorded at different times during continuous scrolling. If the element is only partially visible at the first observation and fully visible at the next, you will see different ratios.I tried this CodePen to simulate that:
I get in the console:
The varying
Intersection Ratio
values logged in the console demonstrate the intersection observer’s behavior as an element is scrolled into and out of view: the intersection ratio for a single target element can indeed change over time, depending on its position relative to the viewport.That would confirm that:
Intersection Ratio
can change dynamically as an element enters or exits the viewport.entries[0].target === entries[1].target
comparison would returntrue
, confirming that it is indeed the same element being observed.Possible reasons:
The
IntersectionObserver
executes its callbacks asynchronously. The browser may batch multiple visibility changes into separate callbacks if these changes occur in rapid succession, like during a scroll.If the observed element or its children are dynamically changing size, being styled, or if other DOM elements are affecting its layout during the scroll, the observer might fire callbacks with differing visibility states.
While the page is continuously scrolling (like with the
.scrollTo()
method), the element’s visibility can change multiple times. Each time it crosses the defined threshold, the observer invokes the callback, resulting in a sequence of entries showing the element’s transition from invisible to fully visible and back to invisible.To address your question directly: even though it seems like the element’s visibility in the DOM should be a binary state, the
IntersectionObserver
is sensitive to the precise moment the observation is made. If the element is in the process of crossing the threshold at the time of observation, the callback might be triggered with anintersectionRatio
of0
(not visible) or1
(fully visible), or any value in between. The element doesn’t have to be removed or added to the DOM for this to happen; it just needs to cross the threshold between being in and out of view.It may happen if you lock the event loop in the task right after the rendering of the first intersection, but before the callback actually fires.
For instance, this snippet will reproduce it:
What happens here is that the intersection observer task is queued at the end of the update the rendering algorithm (current step 18), which is after the
requestAnimationFrame
callbacks are all executed. So our timer is queued first (maybe it also has higher priority, I didn’t check that) and when it is called, it will lock the event loop enough so that at the end of the busy loop the update the rendering would happen again, once again before our intersection observer task because it has less priority.And now the observer has two entries in its queue, the initial one, before the busy loop, and the one after the busy loop.
As you can see from your logs in the screenshot, there is a 55ms difference between both your entries’
time
values. So you certainly faced something of the sort. If it happens regularly, check your dev tools’s performance panel for long frames and try to trace where they come from.