·
5 min read

Lazy Load Vue Component When It Becomes Visible

Lazy Load Vue Component When It Becomes Visible Image

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:

utils.ts
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 definition
  • loadingComponent: 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:

App.vue
<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:

Lazy Load Component

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:

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
Rendering Dynamic Markdown in Nuxt 3+ Image

Rendering Dynamic Markdown in Nuxt 3+

Analyze Memory Leaks in Your Nuxt App Image

Analyze Memory Leaks in Your Nuxt App

Self-Host Your Nuxt App With Coolify Image

Self-Host Your Nuxt App With Coolify

Simpler Two-Way Binding in Vue With defineModel Image

Simpler Two-Way Binding in Vue With defineModel