Editor’s note: This guide to lazy loading in JavaScript was last updated by Iniubong Obonguko on 7 June 2023 to reflect recent changes to JavaScript and include new sections on how to lazy load images, lazy loading best practices, and to include interactive code examples. To take your lazy loading skills to the next level, check out our guide to cross-browser native lazy loading.
In this post, we will look at how lazy loading works in JavaScript. 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 with 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 to easily tell which ads are seen by users who visit their platform and thereby make better business decisions. Let’s get started.
Jump ahead:
According to Wikipedia, lazy loading is a pattern designed to hold off the initialization of an element or an object until it is needed. This means 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 a 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.
Neglecting to lazy load can also cause an increase in page load time due to the size of the bundle to be downloaded/fetched. Low user retention is also mostly applicable in areas with poor internet connectivity. It is not uncommon for users to avoid a platform entirely when developers make the mistake of not implementing lazy loading early on.
And lastly, a large impact on web performance and accessibility is 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 load many images and videos simultaneously on initializing the browser DOM.
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, how 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 learn more about other use cases in real-life environments.
By now, we should better understand why lazy loading web content and assets is necessary. Let’s look at some further advantages of using this technique:
Lazy loading is built on top of the Intersection Observer API. To better understand how lazy loading works, we must first learn about the Intersection Observer web API and how to use it. The Intersection Observer API is a browser API that detects or knows when an element called a target, a parent element, becomes available or visible inside the browser’s viewport, as the case may be. When this occurs, a handler function is invoked to help handle other parts of our code logic.
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 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 browser event almost similar to the document event category, which includes the DOMContentLoaded
event.
For intersection events, we need to specify the element to which we intend to apply the intersection. This element is usually called the
root
element. However, if theroot
is not specified, it means we intend to target the entire browser viewport.
Additionally, we also need to specify a margin for the root
(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 (callback, options);
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
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 null, the parent element automatically becomes the viewport.
Lastly, rootMargin
helps to set the margin around the root
. 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 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 }) }
The intersectionObserverEntry
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");
In the snippet above, we created a target
element and assigned a variable to it. Afterward, we observed the target
using the observe method on the intersectionObserver
constructor/function signature, as shown below:
// start observing for changes on the target element observer.observe(target);
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
to observe. Note that the intersection observer likewise has a bunch of methods in its API: unObserve()
, takeRecords()
, observe()
, etc., are some examples.
In this section, we’ll go over some techniques we can use to incorporate lazy loading in our applications to make rendering images more user-friendly and smooth. First, we can use the default browser attribute. This involves using the loading
attribute, which can be used on <img>
and <iframe>
HTML elements. Setting the loading
to "lazy"
lets the browser know to load the element as it approaches the viewport.
Secondly, we can use the intersection observer. We’ll begin by customizing the options
object for the target element we intend to observe for intersection:
let options = { root: null, // Use the viewport as the root rootMargin: '0px' // Specify the threshold for intersection };
Now, for the target element, let’s target a couple of images:
const lazyImages = document.querySelectorAll(".lazy");
Now, let’s look at implementing the callback:
const handleIntersection = (entries, observer) => { entries.forEach((entry) => { console.log(entry); if (entry.isIntersecting) { const img = entry.target; const src = img.getAttribute("data-src"); // Replace the placeholder with the actual image source img.src = src; // Stop observing the image observer.unobserve(img); } }); };
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, as shown below:
const observer = new IntersectionObserver(handleIntersection, options);
Finally, we can watch the target element to be observed:
lazyImages.forEach((image) => { observer.observe(image); });
Here’s the finished result:
The HTML and CSS code are not included here for simplicity. But, you can find them in the CodeSandbox snippet above. Feel free to fork and play around with the code as much as you like. You can also learn more about the intersection observer API here.
It’s important to note that lazy loading does not only apply to images. In fact, many other page resources can benefit from using lazy loading. Let’s take a look at them:
JavaScript can hinder the rendering of a page by the browser until the content of the script is done loading. This is known as render-blocking. Luckily, JavaScript code can be split up into smaller pieces known as modules. Writing modular JavaScript code can help reduce load time significantly for a page that requires JavaScript to be executed.
CSS code also classifies as a render-blocking resource. Splitting a CSS file into multiple files that only load when necessary can help reduce the time a browser is blocked from rendering the rest of the page. Learn more about optimizing your CSS to prevent render-blocking here.
Inline Frame, or iframes, is an HTML element that loads another element into a webpage. Setting the loading
attribute to "lazy"
on an iframe can help lazy load the content.
Let’s review some lazy loading best practices and discuss when and when not to lazy load resources in our applications. Here are some best practices for using lazy loading with the Intersection Observer API:
placeholder
or data-src
attributes of the elements with the actual source URL of the resource, triggering the network request, and loading the contentloading
attribute to optimize the lazy loading process further. Set the value to "lazy"
to let the browser handle the lazy loading behavior automatically, or set it to "eager"
for critical images that should be loaded immediatelynoscript
variant as they won’t be able to see lazy loaded resourcesNow, the advantages of this technique should be abundantly clear when we have a bunch of images or videos on a webpage and load them all together on initialization of the browser DOM.
As developers, it is our duty to ensure the 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 solve these problems.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Understanding lazy loading in JavaScript"
Thanks for this article. It was really illuminating.
Thanks @gbols. Do let me know if you have any questions.