Lazyload Elements with JavaScript Intersection Observer API

and use cases such as infinite scroll and auto-play media

Image for post
Image for post
Photo by insung yoon on Unsplash

So not long ago, I came across an use case of lazyloading images using npm module (Check it out if you haven’t — it is a small but powerful little thing). Well, convenient as it is, I’m keen to understand the mechanism behind it to unleash its full potential, and so I learnt about Intersection Observer API (I know it’s been out for a while and sorry for the late).

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. — -MDN

The above statement summarises its usage. But what is it needed for? Well, previously when we want to detect if a DOM element enters/leaves viewport so we can do things like lazyloading, etc., we need to make use of scroll events and potentially calling . This can be a performant throttle as calling runs on the main thread and forces the browser to re-layout the entire page and scroll event listeners can fire too many times that will cause the browsing activities sluggish. Not to mention it’s nearly impossible to detect an element’s entering and leaving inside another element, ie. an iframe.

But the Intersection Observer (IO) API solves this by registering a callback function that is executed whenever an element they wish to monitor enters or exits another element (or the viewport) by a requested amount. So the main thread can be free instead of getting buzzed constantly. This is due to the Observer’s nature. Contrary to Event, which queues and fires synchronously and completes, an Observer “observes” and reacts asynchronously.

Let's run a postmortem of the OI API.

var observer = new IntersectionObserver(callback, options);

The object’s constructor takes two parameters. The first one is a callback function that gets fired when an observation point gets triggered. The second optional parameter is options, which is an object with configs about “intersection.”

Options

Options has three properties:

  • – The ancestor element/viewport that the observed element will intersect. This defaults to if unset or the ancestor of the observed element.
  • – Margin around the root, shrinking or growing the root element’s area to watch out for intersection. It’s similar to the CSS property.
  • – An array of values (between 0 and 1.0), each value represents the percentage of such intersection at which the Observer should react, ie. when the callback is to be triggered. Defaults to 0, meaning the callback will run as soon as the element is in view. For an array like it means that the observer should trigger in 4 cases each at 0, 20%, 50% and 100% intersection ratio.

You can pass in the options like:

const config = {
root: null,
rootMargin: '0 200px 0 0',
threshold: [0, 0.5]
};
let observer = new IntersectionObserver(function(entries) {

}, config);

Note that the callback will be fired not only when the observed element intersects with the root element, but also when the first time the observer is initially asked to watch a observed element.

Callback

The callback function takes two arguments, the first is an of with a list of predefined properties regarding the observed elements, and the second is the Observer itself.

cb = function(observerEntries, self) {…};

For properties defined on

  • : A bigger rectangle area for the observed element( + );
  • : A rectangle for the observed element ;
  • : An rectangle for the intersected area;
  • : The observed element;
  • : A indicating the time at which the intersection was recorded, relative to the 's time origin.
  • : The root element;
  • : A Boolean value which is if the target element intersects with the intersection observer's root.
  • The value is a float number indicates how much of the observed element’s area is intersecting with the root element (the ratio of area to area).

All these properties are available in the callback through entries:

let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};

Be aware that your callback is executed on the main thread. It should operate as quickly as possible; if anything time-consuming needs to be done, use .

Also, note that since we need a box-like object wrapping the observed element, it means certain rules have to be obeyed:

  • Elements with cannot be detected;
  • Elements with can be detected;
  • Absolutely positioned elements with can be detected but not if it is completely outside of parent’s borders with negative margins e.g. or cut out by parent’s .

Lazyloading?

So enough of the theory, let’s look at an example.

First register an element/elements for the Observer to watch. In our example, we will try lazyloading some images:

const images = document.querySelectorAll('[data-src]');
///////////////////////
<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”>

The reason we put a is to prevent the image gets loaded when the page gets rendered, instead we let it render a blank (and efficient) for now.

Next, create the callback function to be called when the image lazy loads:

let observer = new IntersectionObserver((entries, observer) => { 
entries.forEach(entry => {
if(entry.isIntersecting){
console.log("start to intersect");
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
}, options);
const options = { rootMargin: '0px 0px 100px 0px' };

Note we use to detect if the images go into viewpoint. Once it enters into viewpoint, we assign the true to its placeholder , and then we unobserve the element after performing the actions on the observed element aka the lazyloading. This is a necessary clean-up step like you do with to prevent memory leaks.

Remember to pass in the object if you need, in our case we set the bottom margin to . This means the picture will start to be intersecting 100px from bottom instead 0px from the bottom. We add this because we want to pre-load the picture instead of only starting to load it when it comes into view.

Next, register the elements to be observed with the observer.

if(!!window.IntersectionObserver){
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
console.log(entry);
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
images.forEach(image => { observer.observe(image);
});
}

Infinite scrolling?

Another example is infinite scroll — whenever the at the end of the page gets scrolled into view, we know it’s the end of the page so load another 10 items.

var intersectionObserver = new IntersectionObserver(   
function (entries) {
if (entries[0].intersectionRatio <= 0)
return;
loadItems(10);//the customised function to load items
console.log('Loaded new items');
});
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);

Auto-pausing videos?

Another use case is to defer the video/ads playing or auto-pause videos/ads when it is out of view, otherwise it would be annoying to hear the sound playing but missing the content.

let video = document.querySelector('video');
let isLeaving = false;
if(!!window.IntersectionObserver){
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.intersectionRatio!=1 && !video.paused){
video.pause();
isLeaving = true;
}
else if(isLeaving) {
video.play();
isLeaving=false
}

});
}, {threshold: 1});
observer.observe(video) ;
}

Note that we have threshold set to 1, so we will only trigger callback function when the video is 100% displayed. And immediately when it’s not 100% displayed we will pause it. We use ` as a reminder as to whether the video is paused or not.

Final remarks

So we can see InteresectionObserver is very useful as an async API in observing DOM elements coming in and out of view (viewpoint or other root elements) and performing a callback task to handle the element. It provides many granular properties on the elements being observed that is convenient for us to manipulate.

However, be aware the callback itself is a synchronous function. If you do perform something really heavy for an element in view, try to use discussed here.

Also, if you are lazy loading assets, remember to or even after the asset has been loaded.

And finally, if you are interested in other Observers, please check below:

That’s so much of it! Happy Reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store