Muhammad Fauzan (fzn0x) - Blog

Lazy loading in JavaScript

Introduction

Lazy Loading in JavaScript is a strategic approach employed to postpone the retrieval of non-essential elements until they are specifically required. By adopting this method, the initial page load performance is enhanced, as it minimizes the upfront download and processing of data. Dynamic asset loading is frequently applied to elements such as images, scripts, and other resources that are not pivotal for the initial rendering of the page. This technique contributes to a more efficient and expedited user experience by prioritizing the loading of essential content first.

For me, there is three types of Lazy Loading as-of-now:

Lazy Asset Loading

Lazy Asset Loading is a strategic approach to postpone the retrieval of non-essential assets, such as CSS and Images.

For images, for example we used the JavaScript events & Infinite Scroll Pagination strategy to achieve this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Loading with Infinite Scroll</title>
    <style>
        #image-container {
            column-count: 3;
            column-gap: 10px;
            margin: 20px;
        }

        .image-item {
            break-inside: avoid-column;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>

<div id="image-container"></div>

<script>
    let page = 1; // Initial page
    const imagesContainer = document.getElementById('image-container');

    function fetchImages(page) {
        // Replace 'your-api-endpoint' with the actual endpoint that returns paginated image data
        const apiUrl = `https://your-api-endpoint?page=${page}`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
                const images = data.images;

                images.forEach(image => {
                    const imageElement = document.createElement('img');
                    imageElement.src = 'placeholder.jpg'; // Use a placeholder image
                    imageElement.setAttribute('data-src', image.url); // Actual image source
                    imageElement.alt = image.alt;
                    imageElement.loading = 'lazy';

                    const imageItem = document.createElement('div');
                    imageItem.classList.add('image-item');
                    imageItem.appendChild(imageElement);

                    imagesContainer.appendChild(imageItem);
                });
            })
            .catch(error => console.error('Error fetching images:', error));
    }

    function loadMoreImages() {
        page++;
        fetchImages(page);
    }

    // Initial load
    fetchImages(page);

    // Infinite scroll
    window.addEventListener('scroll', () => {
        const scrollHeight = document.documentElement.scrollHeight;
        const scrollTop = window.scrollY;
        const clientHeight = window.innerHeight;

        if (scrollTop + clientHeight >= scrollHeight - 200) {
            loadMoreImages();
        }
    });
</script>

</body>
</html>

Each image will load as you scroll down the page. This means that not all the images are displayed at once initially; instead, they load one by one as you scroll through the page. This approach improves the first impression for users accessing your page.

There's another things you can do, for example using Intersection Observer API or native Lazy Loading.

Using Intersection Observer API to load images

Intersection Observer API allows you to efficiently observe changes in the intersection of a target element with an ancestor element or the viewport.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Loading with Intersection Observer API</title>
    <style>
        .image-container {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-around;
            margin: 20px;
        }

        .image-item {
            margin: 10px;
        }
    </style>
</head>
<body>

<div class="image-container" id="image-container">
    <div class="image-item">
        <img src="placeholder.jpg" data-src="lazy-load-image1.jpg" alt="Lazy Load Image 1" loading="lazy">
    </div>
    <div class="image-item">
        <img src="placeholder.jpg" data-src="lazy-load-image2.jpg" alt="Lazy Load Image 2" loading="lazy">
    </div>
</div>

<script>
    const imageContainer = document.getElementById('image-container');

    function handleIntersection(entries, observer) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const lazyImage = entry.target;
                lazyImage.src = lazyImage.dataset.src;
                observer.unobserve(lazyImage);
            }
        });
    }

    function createObserver() {
        const options = {
            root: null,
            rootMargin: '0px',
            threshold: 0.5,
        };

        const observer = new IntersectionObserver(handleIntersection, options);

        // Observe each image in the container
        document.querySelectorAll('.image-item img').forEach(img => {
            observer.observe(img);
        });
    }

    // Create Intersection Observer when the document is ready
    document.addEventListener('DOMContentLoaded', createObserver);
</script>

</body>
</html>

This way, images will load only when they are about to become visible, providing a more efficient and user-friendly experience.

Native Lazy Loading

In Chrome 76 updates, Google add supports for Native Lazy Loading, which available in almost modern chromium-based browsers.

With this browser-side support coming into play, now, we only need to add a "loading" attribute when embedding images, to implement lazy loading on websites.

<!-- Default behavior (equivalent to loading="auto") -->
<img src="example.jpg" alt="Example Image">

<!-- Lazy loading -->
<img src="placeholder.jpg" data-src="lazy-load-image.jpg" alt="Lazy Load Image" loading="lazy">

<!-- Eager loading -->
<img src="important-image.jpg" alt="Important Image" loading="eager">

Note: You should avoid to use the loading=lazy for any images within initial viewport. Click here for more information, always prefer to use loading="eager" for images within initial viewport.

Lazy JS Loading

Lazy JS Loading is a strategic approach to postpone the retrieval of javascript script.

We are using the same technique, as we have already discussed before here:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Loading Example</title>
</head>
<body>
    <h1>Hello, this is a lazy loading example!</h1>
    <!-- Load the main JavaScript file asynchronously while the page is on load -->
    <script defer src="main.js"></script>
</body>
</html>
// main.js

document.addEventListener('DOMContentLoaded', function() {
    // Your JavaScript code goes here
    console.log('Main script loaded and executed.');
});

For Node.js, you can implement something similar using the "Lazy Module Loading" technique:

We will create the module first.

// myModule.js

// Some functionality or variables within the module
let count = 0;

// The initialize function that might be called after the module is loaded
function initialize() {
  console.log('Module initialized');
  count++;
}

// Other functions or variables can be defined as well

// Export the necessary parts of the module
module.exports = {
  initialize,
  getCount: () => count,
};
  1. Dynamic Imports (ESM):

If you are using ECMAScript Modules (ESM) by specificy type="module" in package.json in your Node.js application (requires Node.js 13.2.0 or later), you can use dynamic imports for lazy loading:

const loadModule = modulePath => import(modulePath);

// Assuming your module is in the same directory
// note: since import is an asynchronous function, you need to use the `await` keyword to call the function
await loadModule("./myModule.js");
  1. Using require function

You can also use CommonJS and encapsulate the require statement in a function and call that function when you need to load the module:

function loadModule() {
  return require('./myModule');
}

// Call loadModule when needed
const module = loadModule();
module.initialize(); // call initialize function inside myModule

Lazy Function Loading

For function loading, we use the same technique as Lazy JS Loading, for example in ES6:

// module.js
export function lazyLoadedFunction() {
  console.log("This function is lazily loaded.");
}

Then in your main script:

// main.js
let lazyLoadedFunction;

// Function to load the module lazily
function loadLazyModule() {
  import('./module.js').then(module => {
    lazyLoadedFunction = module.lazyLoadedFunction;
    // Now you can use lazyLoadedFunction
    lazyLoadedFunction();
  });
}

// Call the function when needed
loadLazyModule();

In React.js, we use React.lazy to lazy loading every components. Components is a function, we can create the component first:

// MyComponent.jsx
import React from 'react';

const MyComponent = () => {
  return <div>This is a lazy-loaded component!</div>;
};

export default MyComponent;

Now, in your main component, you can use React.lazy:

// App.js
import React, { lazy, Suspense } from 'react';

// Lazy-loaded component
const MyComponent = lazy(() => import('./MyComponent'));

const App = () => {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
};

export default App;

Now, the components are only loaded when needed. You can do the same thing with React Router to avoid load all pages or components on the first page load:

// App.js
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

const App = () => {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route path="/" exact component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </Suspense>
    </Router>
  );
};

export default App;

I think that's it for Lazy Loading in JavaScript. Always be careful when using lazy loading, as it can make your page faster but also has the potential to make it slower. Only lazy load what matters. Always check the network tab of your browser to find out which one you need to lazy load in your website.