IntersectionObserver API

IntersectionObserver API

When developing web pages, it is often necessary to know whether an element has entered the “viewport”, that is, whether the user can see it or not.

The green square in the image above is constantly scrolling, and its visibility is indicated at the top.

The traditional implementation method is to call getBoundingClientRect() method to get the coordinates corresponding to the upper left corner of the viewport, and then determine whether it is within the viewport. The disadvantage of this method is that due to the intensive occurrence of scroll events, the amount of calculation is very large, and it is easy to cause performance problems.

IntersectionObserver API, which can automatically “observe” whether an element is visible, Chrome 51+ already supports it. Due to the nature of visible (visible), the target element and the viewport produce an intersection, so this API is called “intersection observer” (intersection observerserver).

Introduction

The usage of the IntersectionObserver API is simply two lines.

var observer = new IntersectionObserver(callback, options);
observer.observe(target);

In the above code, IntersectionObserver is a constructor provided natively by the browser and accepts two parameters: callback is the callback function when the visibility changes, and option is the configuration object (this parameter is optional).

The return value of IntersectionObserver() is an observer instance. The instance’s observe() method can specify which DOM node to observe.

// start observing
observer.observe(document.getElementById('example'));

// stop watching
observer.unobserve(element);

// close the observer
observer.disconnect();

In the above code, the parameter of observe() is a DOM node object. If you want to observe multiple nodes, you need to call this method multiple times.

observer.observe(elementA);
observer.observe(elementB);

Note that the IntersectionObserver API is asynchronous and does not fire synchronously as the target element scrolls. The specification states that the implementation of IntersectionObserver should use requestIdleCallback(), that is, the observer will only be executed when the thread is idle. This means that the priority of this observer is very low, and it will only be executed when other tasks are finished and the browser is idle.

IntersectionObserver.observe()

The observe() method of an IntersectionObserver instance is used to initiate the observation of a DOM element. This method accepts two parameters: the callback function callback and the configuration object options.

callback parameter

When the visibility of the target element changes, the observer’s callback function callback is called.

callback fires twice. Once when the target element has just entered the viewport (starting visible), and once when it leaves the viewport completely (starting invisible).

var observer = new IntersectionObserver(
  (entries, observer) => {
    console.log(entries);
  }
);

In the above code, the callback function is written in arrow function. The argument of the callback function (entries) is an array, each member is an IntersectionObserverEntry object (see below for details). For example, if the visibility of two observed objects changes at the same time, the entries array will have two members.

IntersectionObserverEntry object

The IntersectionObserverEntry object provides information about the target element and has a total of six properties.

{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}

The meaning of each attribute is as follows.

  • time: the time when the visibility changed, is a high precision timestamp in milliseconds
  • target: the target element to be observed, is a DOM node object
  • rootBounds: information about the rectangular area of ​​the container element, the return value of the getBoundingClientRect() method, or null if there is no container element (ie scrolling directly relative to the viewport)
  • boundingClientRect: information about the rectangular area of ​​the target element
  • intersectionRect: information about the intersection area of ​​the target element and the viewport (or container element)
  • intersectionRatio: The visible ratio of the target element, that is, the ratio of intersectionRect to boundingClientRect, 1 when it is completely visible, and less than or equal to 0 when it is completely invisible

In the image above, the gray horizontal boxes represent the viewport, and the dark red areas represent the four observed target elements. Their respective intersectionRatio figures are noted.

I wrote a Demo that demonstrates the IntersectionObserverEntry object. Note that this demo will only run on Chrome 51+.

Option object

The second parameter of the IntersectionObserver constructor is a configuration object. It can set the following properties.

(1) Threshold attribute

The threshold property determines when the callback function is triggered, that is, when the element enters the viewport (or container element) at what proportion, the callback function is executed. It is an array, each member is a threshold value, the default is [0], that is, the callback function is triggered when the intersection ratio (intersectionRatio) reaches 0.

If the threshold property is 0.5, the callback function is fired when the element enters 50% of the viewport. If the value is [0.3, 0.6], the callback function is fired when the element enters 30% and 60%.

new IntersectionObserver(
  entries => {/* … */},
  {
    threshold: [0, 0.25, 0.5, 0.75, 1]
  }
);

The user can customize this array. For example, [0, 0.25, 0.5, 0.75, 1] in the above example means that when the target element is 0%, 25%, 50%, 75%, 100% visible, the callback function will be triggered.

(2) root attribute, rootMargin attribute

The IntersectionObserver can observe not only the visibility of elements relative to the viewport, but also the visibility of elements relative to their container. Scrolling within a container also affects the visibility of the target element, see the diagram at the beginning of this article.

The IntersectionObserver API supports scrolling within a container. The root attribute specifies the container node where the target element is located. Note that the container element must be an ancestor of the target element.

var opts = {
  root: document.querySelector('.container'),
  rootMargin: '0px 0px -200px 0px'
};

var observer = new IntersectionObserver(
  callback,
  opts
);

In the above code, in addition to the root property, there is also the rootMargin property. This property is used to expand or shrink the size of the rootBounds rectangle, thereby affecting the size of the intersectionRect intersection area. Its writing is similar to the margin property of CSS, such as 0px 0px 0px 0px, which represent the values ​​of top, right, bottom and left in turn.

The 0px 0px -200px 0px in the above example means that the bottom edge of the container is shrunk upward by 200 pixels. When the page is scrolled down, the callback function will not be triggered until the top of the target element enters the visible area by 200 pixels.

After this setting, whether it is window scrolling or scrolling within the container, as long as the visibility of the target element changes, the observer will be triggered.

Example

Lazy load

Sometimes, we want certain static resources (such as images) to be loaded only when the user scrolls down and they enter the viewport, which can save bandwidth and improve web page performance. This is called “lazy loading”.

With the IntersectionObserver API, this is easy to implement. The HTML code for the image can be written as follows.

<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">

In the above code, the image displays a placeholder by default, and the data-src attribute is the real image that is lazy loaded.

function query(selector) {
  return Array.from(document.querySelectorAll(selector));
}

var observer = new IntersectionObserver(
  function(entries) {
    entries.forEach(function(entry) {
      entry.target.src = entry.target.dataset.src;
      observer.unobserve(entry.target);
    });
  }
);

query('.lazy-loaded').forEach(function (item) {
  observer.observe(item);
});

In the code above, the actual image file is loaded only when the image becomes visible.

Infinite scroll

Infinite scroll (infinite scroll) refers to the continuous loading of new content to the page as the page scrolls to the bottom, and its implementation is also very simple.

var intersectionObserver = new IntersectionObserver(
  function (entries) {
    // if not visible, return
    if (entries[0].intersectionRatio <= 0) return;
    loadItems(10);
    console.log('Loaded new items');
  }
);

// start observing
intersectionObserver.observe(
  document.querySelector('.scrollerFooter')
);

With infinite scrolling, it’s best to have a footer bar at the bottom of the page (aka sentinels, .scrollerFooter in the example above), as in the example above. Once the footer bar is visible, it means that the user has reached the bottom of the page, loading new items before the footer bar. Otherwise, every time new content is added to the page, the observe() method needs to be called to establish an observation at the bottom of the new content.

Video autoplay

Below is a video element, I want it to play automatically when it fully enters the viewport, and automatically pause when it leaves the viewport.

<video src="foo.mp4" controls=""></video>

Below is the JS code.

let video = document.querySelector('video');
let isPaused = false;

let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.intersectionRatio != 1  && !video.paused) {
      video.pause();
      isPaused = true;
    } else if (isPaused) {
      video.play();
      isPaused=false;
    }
  });
}, {threshold: 1});

observer.observe(video);

In the above code, the second parameter of IntersectionObserver() is the configuration object, and its threshold property is equal to 1, that is, the callback function is triggered when the target element is fully visible.

Reference link