Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

Understanding lazy loading in JavaScript

9 min read 2520

Understanding Lazy Loading in JavaScript

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:

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

Advantages of lazy loading

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:

  • Building web applications that are highly accessible: Web accessibility is extremely important. Using this technique helps to build 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 will thank you later!
  • Infinite scroll: 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

Native lazy loading API

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.

Creating an intersection observer

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 the root 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

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.

Implementing lazy loading

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) => {
    if (entry.isIntersecting) {
      const img =;
      const src = img.getAttribute("data-src");
      // Replace the placeholder with the actual image source
      img.src = src;
      // Stop observing the image

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) => {

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.

What other page resources can use lazy loading?

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

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.

Lazy loading best practices

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:

  • Identify the elements to lazy load: Determine which resources on your webpage are non-critical and can be deferred until they come into view or are about to be interacted with by the user, such as off-screen images or below-the-fold content
  • Set up the Intersection Observer: Create an instance of the Intersection Observer API and configure it to observe the target elements. This involves specifying a callback function that will be triggered when the observed elements intersect with a specified root element or the viewport
  • Implement the lazy loading logic: In the Intersection Observer callback function, handle the logic for loading the resources when they become visible. This typically involves replacing the placeholder or data-src attributes of the elements with the actual source URL of the resource, triggering the network request, and loading the content
  • Use appropriate loading attributes: For images, consider using the loading 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 immediately
  • Provide fallback content: While lazy loading improves performance, ensuring a good UX is essential. Provide fallback content, such as placeholders or low-resolution versions of the resources, so users can still understand the context while waiting for the full content to load. Additionally, users that disable JavaScript on their browsers offer a noscript variant as they won’t be able to see lazy loaded resources

When to lazy load resources

  • Large images or media files: Lazy loading is particularly useful for images or media files that are not immediately visible to the user, reducing the initial page load time and bandwidth usage
  • Infinite scroll or dynamically loaded content: When implementing infinite scroll or loading content dynamically as the user scrolls, lazy loading ensures that only the necessary content is loaded as it becomes visible
  • Performance optimization: Lazy loading can significantly improve the performance of webpages, especially those with a large amount of content or resources

When to avoid lazy loading

  • Critical resources: Avoid lazy loading resources crucial for the initial page rendering or the UX. This includes content above the fold, important scripts, or interactive elements necessary for the core functionality of the page
  • Small or fast-loading resources: If the resources are small in size or load quickly, the benefits of lazy loading may be negligible, and it may be more efficient to load them immediately
  • SEO considerations: Search engine crawlers may not execute JavaScript or wait for lazy loaded content to load. If the content you want to lazy load is essential for SEO or indexing purposes, consider alternative methods, such as preloading or server-side rendering


Now, 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.

LogRocket: Debug JavaScript errors more easily by understanding the context

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 find out exactly what the user did that led to an error.

LogRocket Dashboard Free Trial Banner

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.
Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

2 Replies to “Understanding lazy loading in JavaScript”

Leave a Reply