Lazy loading of Vue components

In today’s fast-paced digital world, website performance is critical to engaging users and achieving success. However, for pages like the homepage, optimizing performance without compromising functionality becomes a challenge.

This is where lazy loading of Vue components comes in. By deferring the loading of non-essential elements until they are visible, developers can enhance the user experience while ensuring fast loading of landing pages.

Lazy loading is a technique that prioritizes loading of critical content while deferring loading of less important elements. This approach not only shortens the initial load time of the page, but also conserves network resources, resulting in a lighter and more responsive user interface.

In this article, I will show you a simple mechanism to lazy load Vue components when they are visible using the Intersection Observer API .

Intersection Observer API

The Intersection Observer API is a powerful tool that allows developers to efficiently track and respond to changes in the visibility of elements in the browser viewport.

It provides a way to asynchronously observe the intersection between an element and its parent element, or between an element and the viewport. It provides a performant, optimized solution for detecting when an element is visible or hidden, reducing the need for inefficient scroll event listeners and enabling developers to selectively load or manipulate content when necessary, thereby enhancing the user experience.

It is often used to implement features such as infinite scrolling and lazy loading of images.

Asynchronous components

Vue 3 provides defineAsyncComponent for asynchronously loading components only when needed.

It returns a Promise defined by the component:

import { defineAsyncComponent } from  'vue'

const  AsyncComp = defineAsyncComponent ( () => {
   return  new  Promise ( ( resolve, reject ) => {
     // ...load component from server 
    resolve ( /* loaded component */ )
  })
})

Errors and loading status can also be handled:

const  AsyncComp = defineAsyncComponent ({
   // the loader function 
  loader : () =>  import ( './Foo.vue' ),

  // A component to use while the async component is loading 
  loadingComponent : LoadingComponent ,
   // Delay before showing the loading component. Default: 200ms. 
  delay : 200 ,

  // A component to use if the load fails 
  errorComponent : ErrorComponent ,
   // The error component will be displayed if a timeout is 
  // provided and exceeded. Default: Infinity. 
  timeout : 3000 
})

We will use this function to load the component asynchronously when it is visible.

Lazy loading of components

Now, let’s combine the Intersection Observer API and defineAsyncComponentfunctions to asynchronously load components when they are visible:

import {
  h,
  defineAsyncComponent,
  defineComponent,
  ref,
  onMounted,
  AsyncComponentLoader ,
   Component ,
} from  'vue' ;

type ComponentResolver = ( component: Component ) =>  void

export  const  lazyLoadComponentIfVisible = ( {
  componentLoader,
  loadingComponent,
  errorComponent,
  delay,
  timeout
}: {
  componentLoader: AsyncComponentLoader;
  loadingComponent: Component;
  errorComponent?: Component;
  delay?: number;
  timeout?: number;
} ) => {
   let  resolveComponent : ComponentResolver ;

  return  defineAsyncComponent ({
     // the loader function 
    loader : () => {
       return  new  Promise ( ( resolve ) => {
         // We assign the resolve function to a variable 
        // that we can call later inside the loadingComponent 
        // when the component becomes visible 
        resolveComponent = resolve as  ComponentResolver ;
      });
    },
    // A component to use while the async component is loading 
    loadingComponent : defineComponent ({
       setup () {
         // We create a ref to the root element of 
        // the loading component 
        const elRef = ref ();

        async  function  loadComponent () {
             // `resolveComponent()` receives the 
            // the result of the dynamic `import()` 
            // that is returned from `componentLoader()` 
            const component = await  componentLoader ()
             resolveComponent (component)
        }

        onMounted ( async () => {
           // We immediately load the component if 
          // IntersectionObserver is not supported 
          if (!( 'IntersectionObserver'  in  window )) {
             await  loadComponent ();
             return ;
          }

          const observer = new  IntersectionObserver ( ( entries ) => {
             if (!entries[ 0 ]. isIntersecting ) {
               return ;
            }

            // We cleanup the observer when the 
            // component is not visible anymore 
            observer. unobserve (elRef. value );
             await  loadComponent ();
          });

          // We observe the root of the 
          // mounted loading component to detect 
          // when it becomes visible 
          observer. observe (elRef. value );
        });

        return  () => {
           return  h ( 'div' , { ref : elRef }, loadingComponent);
        };
      },
    }),
    // Delay before showing the loading component. Default: 200ms.
    delay,
    // A component to use if the load fails
    errorComponent,
    // The error component will be displayed if a timeout is 
    // provided and exceeded. Default: Infinity.
    timeout,
  });
};

Let’s break down the code above:

We create a lazyLoadComponentIfVisiblefunction that accepts the following parameters:

  • componentLoader: Returns a function that resolves to the Promise defined by the component
  • loadingComponent: Component used when loading asynchronous components.
  • errorComponent: Component used when loading fails.
  • delay: Shows the delay before loading the component. Default value: 200 milliseconds.
  • timeout: If a timeout is provided, the error component will be displayed. default value: Infinity.

Function returns defineAsyncComponenta function that contains logic to asynchronously load the component when it is visible.

The main logic happens defineAsyncComponentinside loadingComponent:

We use defineComponentto create a new component that contains a render function that renders in the passed lazyLoadComponentIfVisibleto . The render function contains a template pointing to the root element of the loading component .divloadingComponentref

In onMounted, we check IntersectionObserverif is supported. If it is not supported, we will load the component immediately. Otherwise, we will create one IntersectionObserverthat watches the root element of the loaded component to detect when it becomes visible. When the component becomes visible, we clean up the observer and load the component.

Now, you can use this function to lazy load the component when it is visible:

<script setup lang= "ts" >
 import  Loading  from  './components/Loading.vue' ;
 import { lazyLoadComponentIfVisible } from  './utils' ;

const  LazyLoaded = lazyLoadComponentIfVisible ({
   componentLoader : () =>  import ( './components/HelloWorld.vue' ),
   loadingComponent : Loading ,
});
</script>

< template > 
  < LazyLoaded /> 
</ template >

Summarize

In this article, we learned how to use the Intersection Observer API and defineAsyncComponentfunctions to lazily load Vue components when they are visible. This is useful if you have a homepage with many components and want to improve the initial load time of your application.