
In today's fast-paced digital world, website performance is crucial for engaging users and achieving online success. Landing pages, serving as the virtual storefronts of businesses, hold immense importance in capturing audience attention and driving conversions. However, when it comes to large sites like landing pages, performance optimization becomes a challenge without compromising functionality.
That's where lazy loading Vue components come in. By deferring the loading of non-essential elements until they are visible, developers can enhance the user experience while ensuring swift load times on vital landing pages.
Lazy loading is a technique that prioritizes the initial rendering of critical content while postponing the loading of secondary elements. This approach not only reduces the initial page load time but also conserves network resources, resulting in a snappier and more responsive user interface.
In this blog post, I'll show you a simple mechanism to lazy load your Vue components if they become 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 within the browser's viewport.
It provides a way to asynchronously observe intersections between an element and its parent, or between an element and the viewport. It offers a performant and optimized solution for detecting when elements become visible or hidden, reducing the need for inefficient scroll event listeners and enabling developers to enhance user experiences by selectively loading or manipulating content precisely when it becomes necessary.
It is typically used to implement features such as infinite scrolling and image lazy loading.
Async Components
Vue 3 provides a defineAsyncComponent to asynchronously load components only when they are needed.
It returns a Promise that resolves to a component definition:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...load component from server
resolve(/* loaded component */)
})
})
It is also possible to handle error and loading states:
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 functionality to load our components asynchronously when they become visible.
Lazy Loading Components When They Become Visible
Let's now combine the Intersection Observer API and the defineAsyncComponent
function to load our components asynchronously when they become 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
: A function that returns a Promise that resolves to a component definitionloadingComponent
: A component to use while the async component is loading.errorComponent
: A component to use if the load fails.delay
: Delay before showing the loading component. Default: 200ms.timeout
: The error component will be displayed if a timeout is provided and exceeded. Default: Infinity.
The function returns defineAsyncComponent
which includes the logic to load the component asynchronously when it becomes visible.
The main logic happens in loadingComponent
inside of defineAsyncComponent
:
We create a new component using defineComponent
which includes a render function that renders the loadingComponent
inside a wrapper div
that was passed to lazyLoadComponentIfVisible
. The render function includes a template ref to the root element of the loading component.
Inside onMounted
we check if the IntersectionObserver
is supported. If it is not supported, we immediately load the component. Otherwise, we create an IntersectionObserver
that observes the root element of the mounted loading component to detect when it becomes visible. When the component becomes visible, we cleanup the observer and load the component.
You can now use this function to lazy load your components when they become 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>
StackBlitz Demo
Try it yourself in the following StackBlitz demo:
If you scroll the page down until the component becomes visible, you will see in the Network tab in your browser DevTools that the component is loaded asynchronously:
Conclusion
In this article, you learned how to lazy load Vue components when they become visible using the Intersection Observer API and the defineAsyncComponent
function. This can be useful if you have a landing page with many components and want to improve the initial load time of your application.
Special thanks to Markus Oberlehner who wrote a similar article for Vue 2 which inspired me to write this article for Vue 3.
If you liked this article, follow me on Twitter to get notified about new blog posts and more content from me.
Alternatively (or additionally), you can subscribe to my weekly Vue newsletter: