Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intersection Observer fails sometimes when i scroll fast

I have a little issue with the Intersection Observer API, It works good for me, but.

When I scroll fast (very fast) on my webpage, the Intersection Observer API fails sometimes to detect the pretense of an element in the viewport.

When I scroll slow/normally it works for all elements

Observer options:

RootMargin: 0px 0px -40px 0px,
threshold: 0.7,
root: null

Height of elements : between 100px and 200px

Anyone know why?

like image 336
Zarma Tech Avatar asked May 22 '20 09:05

Zarma Tech


People also ask

Is intersection observer async?

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

Can intersection observer observe multiple elements?

observe() The IntersectionObserver method observe() adds an element to the set of target elements being watched by the IntersectionObserver . One observer has one set of thresholds and one root, but can watch multiple target elements for visibility changes in keeping with those.

What is intersection observer used for?

The IntersectionObserver interface can be used to observe changes in the intersection of an intersection root and one or more target Element s.

What is intersection observer safari?

Each intersection observer entry is a dictionary containing details about the intersection between a particular target and the root. This includes a timestamp, the intersection rectangle, and an intersection ratio that represents the fraction of the target that's contained within the intersection rectangle.


1 Answers

Intersection Observer runs an asynchronous function in a loop which checks the position of observed DOM elements. It's coupled with the browser's render cycle and although it's happening very fast (60fps for most devices, or once every 16.66 miliseconds), if you move the scrollbar faster than those checks happen, the IO API may not detect some visibility changes. In fact, the elements which have not been detected haven't even been rendered.

Which makes sense, because the primary goal of the IO is to check if an element is visible to the human eye. According to the Intersection Observer spec the aim is to provide a simple and optimal solution to defer or preload images and lists, detect e-commerce ads visibility, etc.

That being said, Intersection Observer is a long awaited feature and it solves a great deal of problems, but it is not ideal for all use cases. If you can live with the fact that in high-velocity scrolling situations it will not capture some intersections - great. If not, well, there are other options.

Solution 1: Inferring intersecting elements

One way of handling this issue is trying to indirectly infer which elements crossed the viewport but were not captured by the Intersection Observer. To implement it, you will need to give a unique numeric attribute to all your list elements in an ascending order. Then, within each Intersection Observer's callback function, save the min and max IDs of intersecting elements. At the end of the callback, call setTimeout(applyChanges, 150) to schedule a function which will loop through all elements with IDs between min and max were not omitted by the IO. Also, put clearTimeout() at the beginning of callback, to ensure that this function waits until IO is inactive for some small amount of time.

let minId = null;
let maxId = null;
let debounceTimeout = null;

function applyChanges() {
  console.log(minId, maxId);
  const items = document.querySelectorAll('.item');
  // perform action on elements with Id between min and max
  minId = null;
  maxId = null;
}

function reportIntersection(entries) {
  clearTimeout(debounceTimeout);
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const entryId = parseInt(entry.target.id);
      if (minId === null || maxId === null) {
        minId = entryId;
        maxId = entryId;
      } else {
        minId = Math.min(minId, entryId);
        maxId = Math.max(maxId, entryId);
      }
    }
  });
  debounceTimeout = setTimeout(applyChanges, 500);
}

const container = document.querySelector('#container');
const items = document.querySelectorAll('.item');
const io = new IntersectionObserver(reportIntersection, container);
let idCounter = 0;
items.forEach(item => {
  item.setAttribute('id', idCounter++);
  io.observe(item)
});

Solution 2: Throttling scroll events

There’s no possibility to influence the default scrollbar behaviour, but you could override it with a custom scrollbar. The idea is to throttle the scroll event, so that the container can’t scroll more than its own height every render cycle. This way, the IntersectionObserver will have time to capture all intersections. Although I’d recommend using existing libraries for this task, below is a rough logic of how it should work.

First, define the maximum distance a container can scroll within a single render cycle: let scrollPool = 500. On each scroll event, schedule resetting the scrollPool to the original value during next render in requestAnimationFrame() function. Next, check if the scrollPool is less than 0 and if it is, return without scrolling. Otherwise, subtract the scrollDistance from the scrollPool and add the scrollDistance to the container’s scrollTop property.

Again, this solution might be an overkill to implement because of the multitude of ways a user can scroll a page. But in order to depict the idea here's a draft for the wheel event:

#container { overflow: hidden; }
let scrollPool = 500;

function resetScrollPool() {
  scrollPool = 500;
}

function scrollThrottle(event) {
  window.requestAnimationFrame(resetScrollPool);
  if (scrollPool < 0) {
    return false;
  }
  const scrollDistance = event.deltaY * 10;
  scrollPool = scrollPool - Math.abs(scrollDistance);
  document.querySelector('#container').scrollTop += scrollDistance;
}

window.addEventListener('wheel', scrollThrottle);

Solution 3: Implementing custom intersection detection

This would mean ditching the IntersectionObserver altogether and using old methods. This would definitely be much more costly performance-wise, but you would be sure to capture all elements which crossed the viewport. The idea is to add a scroll handler function to loop through all children of the container to see if they crossed the visible area. For improved performance, you might want to debounce this function by a second or-so. A great implementation can be found in: How can I tell if a DOM element is visible in the current viewport?

like image 191
83C10 Avatar answered Sep 19 '22 00:09

83C10