IntersectionObserver

When developing a web page, it is often necessary to know whether an element has entered the "viewport", that is, whether the user can see it.

The green square in the picture above keeps scrolling, and its visibility will be indicated at the top.

The traditional way to achieve this is to call the target element (green square) [getBoundingClientRect()](https://developer.mozilla.org/en/docs/Web/API/Element/ after listening to the scroll event getBoundingClientRect) method, 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 and a large amount of calculation, it is easy to cause [performance problems](http://www.ruanyifeng.com/blog/2015/09/web-page-performance-in -depth.html).

IntersectionObserver API, can automatically "observe" whether the element is visible, Chrome 51+ already supports it. Since the essence of visible is that the target element and the viewport produce an intersection, this API is called "intersection oberserver".

Introduction

The usage of IntersectionObserver API is simply two lines.

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

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

The return value of IntersectionObserver() is an observer instance. The observe() method of the instance 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 must call this method multiple times.

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

Note that the IntersectionObserver API is asynchronous and is not triggered synchronously with the scrolling of the target element. The specification states that the implementation of IntersectionObserver should use requestIdleCallback(), that is, the observer will be executed only when the thread is idle. This means that the priority of this observer is very low and will only be executed when other tasks are completed and the browser is free.

IntersectionObserver.observe()

The observe() method of the IntersectionObserver instance is used to start 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 callback function callback of the observer is called.

callback will trigger twice. Once the target element has just entered the viewport (begins visible), and the other time it has left the viewport completely (begins not visible).

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

In the above code, the callback function is written in arrow function. The parameter (entries) of the callback function is an array, and 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. There are six attributes in total.

{
  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 changes. It is a high-precision timestamp in milliseconds
  • target: the target element to be observed, which is a DOM node object
  • rootBounds: information about the rectangular area of ​​the container element, the return value of the getBoundingClientRect() method, if there is no container element (ie scroll directly relative to the viewport), then null
  • boundingClientRect: information about the rectangular area of ​​the target element
  • intersectionRect: information about the intersection area between 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, which is 1 when fully visible, and less than or equal to 0 when completely invisible

In the above figure, the gray horizontal box represents the viewport, and the dark red area represents the four observed target elements. Their respective intersectionRatio diagrams have been marked.

I wrote a Demo to demonstrate the IntersectionObserverEntry object. Note that this Demo can only be 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 attribute determines when the callback function is triggered, that is, when the element enters the viewport (or container element) what percentage, 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, when the element enters the viewport 50%, the callback function is triggered. If the value is [0.3, 0.6], the callback function is triggered 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 0%, 25%, 50%, 75%, 100% is visible, the callback function will be triggered.

(2) root attribute, rootMargin attribute

IntersectionObserver can not only observe the visibility of the element relative to the viewport, but also the visibility of the element relative to its container. Scrolling in the container also affects the visibility of the target element, see the diagram at the beginning of this article.

The IntersectionObserver API supports scrolling within the container. The root attribute specifies the container node where the target element is located. Note that the container element must be the ancestor node 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 attribute, there is also the rootMargin attribute. This attribute is used to expand or reduce the size of the rootBounds rectangle, thereby affecting the size of the intersection area of ​​intersectionRect. Its writing is similar to the margin property of CSS, such as 0px 0px 0px 0px, which in turn represents the values ​​of top, right, bottom and left in the four directions.

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

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

Examples

Lazy load

Sometimes, we hope that certain static resources (such as pictures) will only be loaded when the user scrolls down and enter the viewport, which can save bandwidth and improve web page performance. This is called "lazy loading".

With the IntersectionObserver API, it is easy to implement. The HTML code of 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 lazily 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 above code, the real image file will only be loaded when the image becomes visible.

Infinite scroll

Infinite scroll refers to the continuous loading of new content to the page as the web page scrolls to the bottom. Its implementation is also very simple.

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

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

For infinite scrolling, it is best to have a footer bar at the bottom of the page (also known as sentinels, in the example above is .scrollerFooter) as in the example above. Once the footer bar is visible, it means that the user has reached the bottom of the page, and new items are loaded and placed in front of the footer bar. Otherwise, you need to call the observe() method every time new content is added to the page to establish an observation at the bottom of the new content.

Video auto play

Below is a video element. I hope it will play automatically when it fully enters the viewport, and it will 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.