DEV Community

Cover image for Understanding lazy loading in JavaScript
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Understanding lazy loading in JavaScript

Written by Alexander Nnakwue✏️

Introduction

In this post, we are going to look at how lazy loading works on the web. We will cover the native lazy loading API — how lazy loading is implemented, the importance and advantages of lazy loading, and, finally, a simple use case of lazy loading web content. To effectively follow along this tutorial, it is assumed that readers have a basic understanding of building web applications with JavaScript.

Understanding the lazy loading API and how it works will help developers who already work with libraries and frameworks that implement these techniques to understand what goes on under the hood. Additionally, they’ll be able to perform more guided research and apply the techniques they learn if they ever intend to implement their own lazy loading library.

As for a real-world use case, marketing and advertising firms who make their revenue off advertisements on their platform can easily optimize and apply lazy loading so as to easily tell which ads are seen by users who visit their platform and thereby make better business decisions.

LogRocket Free Trial Banner

What is lazy loading?

According to Wikipedia, lazy loading is a pattern designed to hold off the initialization of an element or an object until it is needed. What this means is that a target DOM element, relative to a parent DOM element, is loaded and becomes visible (when there is an intersection between both elements, based on a set threshold value) only when a user scrolls through them on a webpage.

The disadvantages of not employing this pattern can lead to:

  • Huge lag in page performance due to multiple synchronous network requests or batch requests to fetch a couple of images or other web resources from one or more sources
  • Increase in page load time due to the size of the bundle to be downloaded/fetched
  • Low user retention, mostly applicable in areas with poor internet connectivity. It is not uncommon for users to avoid a platform entirely when we developers make the mistake of not implementing lazy loading early on
  • A huge impact on web performance and accessibility caused by resources or assets like images, iframes, and videos that are not properly handled

Currently, lazy loading is natively supported on the web for most modern and updated browsers. However, for browsers that don’t offer this support yet, polyfills or libraries that implement this technique provide simple API layers above them.

Lazy loading solves the problem of reducing initial page load time — displaying only resources like images or other content that a user needs to see on initializing a webpage and as the page is subsequently scrolled.

Web performance and accessibility issues are known to be multifaceted; reducing page size, memory footprint, and general load time can contribute a great deal to a web platform. The advantages of lazy loading become obvious when we have a bunch of images and videos, and we load them all at once on initialization of the browser DOM. Certainly, you should now have an understanding of what this will lead to, as we have earlier discussed.

Judging by the data, most websites rely heavily on images and other web content like videos or iframes to pass information across to their target audience. While this might seem trivial and simple, the way we display this content to our users determines how performant our platform is at the end of the day.

Furthermore, actions that would help optimize our page load time, like events that are dependent on whether a user scrolls to a particular portion of our webpage, are some of the use cases of lazy loading. As we continue with this article, we will get to learn more about other use cases in real-life environments.

Native lazy loading API

Lazy loading is built on top of the Intersection Observer API, which is a browser API that provides a way of detecting or knowing when an element called a target, a parent element, or becomes available or visible inside the browsers viewport, as the case may be.

When this occurs, a handler function is invoked to help handle other parts of our code logic, as we will see later on. With this new and improved browser API, we can also know when two DOM elements intersect — by this, we mean when a target DOM element enters the browser’s viewport or intersects with another element, which, most likely, is its parent element.

To better understand how lazy loading works, we first of all have to understand how to create an intersection observer. In order to create an intersection observer, all we need to do is listen to the occurrence of an intersection observer event and trigger a callback function or handler to run when this kind of event occurs. The intersection observer event is a kind of browser event that is almost similar to the document event category, which includes the DOMContentLoaded event.

Note: For intersection events, we need to specify the element that we intend to apply the intersection against. This element is usually called the root element. However, if the root element is not specified, it means we intend to target the entire browser viewport.

Additionally, we also need to specify a margin for the root element (if provided) so that we can easily alter its shape or size, if necessary, on intersection. Let’s take a look at an example to understand it better:

let options = {
root: null,
rootMargin: 10px,
threshold: 1.0
}

let observer  = new IntersectionObserver (options, callback);
Enter fullscreen mode Exit fullscreen mode

In the above snippet, we have seen a simple use case for creating an observer. The options object helps us define custom properties for our target.

Here, the threshold property in the options object signifies when the callback is to be triggered. It has a default value of zero, which means that as soon as a user approaches the target element and it becomes visible, the callback is triggered.

On the other hand, the root is the parent element that acts as the viewport for the target element when the target element becomes visible to the user as they scroll through the webpage. Note that if the root is set to null, the parent element becomes the viewport automatically.

Lastly, rootMargin helps to set the margin around the root element. For example, before we compute the intersection between the target and the parent element/viewport, we might have to tweak its size, margin, or dimension.

Furthermore, the callback, which accepts two input parameters, includes a list of intersectionObserverEntry objects we intend to apply on the target element and the observer for which the callback is being invoked.

The signature of the callback is shown below:

let callback = (entries, observer) => {
entries.forEach(entry => {
If (entry.isIntersection) {
// do some magic here
}
// and some other methods
})
}
Enter fullscreen mode Exit fullscreen mode

The intersectionObserverEntry object signifies when there is an intersection between parent and target elements. It has a bunch of properties in its API, which include isIntersection, intersectionRatio, intersectionRect, target, time, etc. For a detailed explanation of these properties, you can consult this section of the MDN documentation.

We need to target a specific DOM element and trigger a callback function when it intersects with a parent element. An example of a DOM element to target is shown in the code snippet below:

let target = document.querySelector("#targetElement");
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we created a target element and assigned a variable to it. Afterwards, we observed the target element using the observe method on the intersectionObserver constructor/function signature, as shown below:

// start observing for changes on the target element

observer.observe(target);
Enter fullscreen mode Exit fullscreen mode

When the threshold set by the observer for the target is reached, the callback is fired. Simple, right?

Lastly, the observe() method tells the observer what target element to observe. Note that the intersection observer likewise has a bunch of methods in its API: unObserve(), takeRecords(), observe(), etc. are some examples.

Advantages of lazy loading technique

By now, we must have a better understanding of why lazy loading web content and assets is necessary. Let’s look at some further advantages of using this technique:

  • Building web applications that are highly accessible. Talks about web accessibility are on the front burner today. Using this technique would definitely aid in building a platform that has a wider reach
  • High user retention. If a web platform is tied to driving business objectives and, in turn, providing value, implementing this technique would help a lot in making the platform user-friendly. The web standards would thank you later!
  • As a developer, you might be tasked with implementing infinite scroll on a web platform. Having an understanding of this concept would help a great deal, thereby providing immediate business value

Implementing lazy loading

Let’s look at a simple example of lazy loading images on a webpage. We’ll begin by customizing the options object for the target element we intend to observe for intersection against:

let  options = {
root: document.querySelector('.root'),
rootMargin: '0px, 0px, 100px, 0px'
};
Enter fullscreen mode Exit fullscreen mode

Now, for the target element, let’s target a couple images:

let  images = [...document.querySelectorAll('.targetImages')];
Enter fullscreen mode Exit fullscreen mode

Now, let’s look at implementing the callback:

const callback = (entries) => {

entries.forEach(entry => {
 If (entry.isIntersecting) {
    observer.unObserve('entry.target');
}
// handle other code logic here 
})
}
Enter fullscreen mode Exit fullscreen mode

We can go ahead and call the intersection observer constructor function to observe the target element based on the customizations specified in its options object:

let observer = new intersectionObserver(options, callback);
Enter fullscreen mode Exit fullscreen mode

Finally, we can watch the target element to be observed:

images.forEach(image => {
observer.observe(image);
})
Enter fullscreen mode Exit fullscreen mode

Note: The HTML and CSS code are not included here for simplicity. You can get a detailed feel of how to implement this technique by checking this example in the MDN docs.

Summary

Now the advantages of this technique should be abundantly clear when we have a bunch of images or videos on a webpage and we load them all together on initialization of the browser DOM. As developers, it is our duty to ensure optimal performance of the platforms we manage or maintain, especially if they are tied to business objectives. Lazy loading as a web performance technique helps to solve these kinds of problems.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Understanding lazy loading in JavaScript appeared first on LogRocket Blog.

Top comments (0)