skip to Main Content

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);

enter image description here

2

Answers


  1. 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:

    document.addEventListener("DOMContentLoaded", (event) => {
      const rootElement = document.getElementById("root");
      const targetElement = document.getElementById("targetElement");
    
      const intersectionObserverOptions = {
        root: rootElement,
        rootMargin: "0px",
        threshold: Array.from({ length: 100 }, (_, index) => index * 0.01)
      };
    
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          console.log(`Intersection Ratio: ${entry.intersectionRatio}`);
        });
      }, intersectionObserverOptions);
    
      observer.observe(targetElement);
    
      setInterval(() => {
        rootElement.scrollBy({ top: 5 });
      }, 100);
    });
    

    I get in the console:

    "Intersection Ratio: 0"
    "Intersection Ratio: 0"
    "Intersection Ratio: 0.25"
    "Intersection Ratio: 0.5"
    "Intersection Ratio: 0.75"
    "Intersection Ratio: 1"
    "Intersection Ratio: 0.75"
    "Intersection Ratio: 0.5"
    "Intersection Ratio: 0.25"
    "Intersection Ratio: 0"
    "Intersection Ratio: 0"
    

    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:

    • The Intersection Ratio can change dynamically as an element enters or exits the viewport.
    • Even for the same element, you might get multiple observations with different ratios if you check the values over a period of time while scrolling.
    • The entries[0].target === entries[1].target comparison would return true, confirming that it is indeed the same element being observed.

    I am looking to understand any possible reasons why a single threshold would result in two entries, one which says the element is visible in DOM and the other that says it isn’t.

    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 an intersectionRatio of 0 (not visible) or 1 (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.

    Login or Signup to reply.
  2. 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:

    setTimeout(() => { // avoid Firefox's weird setTimeout priority at page load
    const target = document.querySelector(".target");
    const observer = new IntersectionObserver((entries) => {
      console.log(entries.length);
      console.log(entries.map((e) => e.isIntersecting));
    });
    observer.observe(target); // register in this frame
    requestAnimationFrame(() => {
      setTimeout(() => { // after paint
        target.style.display = "block";
        const t1 = performance.now();
        while (performance.now() - t1 < 300) {
          // lock the event loop
        }
        console.log("after lock"); // Will be output before the intersection callback fires
      })
    });
    }, 100)
    .target {
      display: none
    }
    <div class=target></div>

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search