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 millisecondstarget
: the target element to be observed, is a DOM node objectrootBounds
: information about the rectangular area of the container element, the return value of thegetBoundingClientRect()
method, ornull
if there is no container element (ie scrolling directly relative to the viewport)boundingClientRect
: information about the rectangular area of the target elementintersectionRect
: 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 ofintersectionRect
toboundingClientRect
,1
when it is completely visible, and less than or equal to0
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.