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 defineAsyncComponent
functions 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 lazyLoadComponentIfVisible
function that accepts the following parameters:
componentLoader
: Returns a function that resolves to the Promise defined by the componentloadingComponent
: 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 defineAsyncComponent
a function that contains logic to asynchronously load the component when it is visible.
The main logic happens defineAsyncComponent
inside loadingComponent
:
We use defineComponent
to create a new component that contains a render function that renders in the passed lazyLoadComponentIfVisible
to . The render function contains a template pointing to the root element of the loading component .div
loadingComponent
ref
In onMounted
, we check IntersectionObserver
if is supported. If it is not supported, we will load the component immediately. Otherwise, we will create one IntersectionObserver
that 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 defineAsyncComponent
functions 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.