Javascript is required
·
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:

1import { defineAsyncComponent } from 'vue'
2
3const AsyncComp = defineAsyncComponent(() => {
4  return new Promise((resolve, reject) => {
5    // ...load component from server
6    resolve(/* loaded component */)
7  })
8})

It is also possible to handle error and loading states:

1const AsyncComp = defineAsyncComponent({
2  // the loader function
3  loader: () => import('./Foo.vue'),
4
5  // A component to use while the async component is loading
6  loadingComponent: LoadingComponent,
7  // Delay before showing the loading component. Default: 200ms.
8  delay: 200,
9
10  // A component to use if the load fails
11  errorComponent: ErrorComponent,
12  // The error component will be displayed if a timeout is
13  // provided and exceeded. Default: Infinity.
14  timeout: 3000
15})

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
1import {
2  h,
3  defineAsyncComponent,
4  defineComponent,
5  ref,
6  onMounted,
7  AsyncComponentLoader,
8  Component,
9} from 'vue';
10
11type ComponentResolver = (component: Component) => void
12
13export const lazyLoadComponentIfVisible = ({
14  componentLoader,
15  loadingComponent,
16  errorComponent,
17  delay,
18  timeout
19}: {
20  componentLoader: AsyncComponentLoader;
21  loadingComponent: Component;
22  errorComponent?: Component;
23  delay?: number;
24  timeout?: number;
25}) => {
26  let resolveComponent: ComponentResolver;
27
28  return defineAsyncComponent({
29    // the loader function
30    loader: () => {
31      return new Promise((resolve) => {
32        // We assign the resolve function to a variable
33        // that we can call later inside the loadingComponent 
34        // when the component becomes visible
35        resolveComponent = resolve as ComponentResolver;
36      });
37    },
38    // A component to use while the async component is loading
39    loadingComponent: defineComponent({
40      setup() {
41        // We create a ref to the root element of 
42        // the loading component
43        const elRef = ref();
44
45        async function loadComponent() {
46            // `resolveComponent()` receives the
47            // the result of the dynamic `import()`
48            // that is returned from `componentLoader()`
49            const component = await componentLoader()
50            resolveComponent(component)
51        }
52
53        onMounted(async() => {
54          // We immediately load the component if
55          // IntersectionObserver is not supported
56          if (!('IntersectionObserver' in window)) {
57            await loadComponent();
58            return;
59          }
60
61          const observer = new IntersectionObserver((entries) => {
62            if (!entries[0].isIntersecting) {
63              return;
64            }
65
66            // We cleanup the observer when the 
67            // component is not visible anymore
68            observer.unobserve(elRef.value);
69            await loadComponent();
70          });
71
72          // We observe the root of the
73          // mounted loading component to detect
74          // when it becomes visible
75          observer.observe(elRef.value);
76        });
77
78        return () => {
79          return h('div', { ref: elRef }, loadingComponent);
80        };
81      },
82    }),
83    // Delay before showing the loading component. Default: 200ms.
84    delay,
85    // A component to use if the load fails
86    errorComponent,
87    // The error component will be displayed if a timeout is
88    // provided and exceeded. Default: Infinity.
89    timeout,
90  });
91};

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
1<script setup lang="ts">
2import Loading from './components/Loading.vue';
3import { lazyLoadComponentIfVisible } from './utils';
4
5const LazyLoaded = lazyLoadComponentIfVisible({
6  componentLoader: () => import('./components/HelloWorld.vue'),
7  loadingComponent: Loading,
8});
9</script>
10
11<template>
12  <LazyLoaded />
13</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.
Simpler Two-Way Binding in Vue With defineModel Image

Simpler Two-Way Binding in Vue With defineModel

Unlocking the Power of v-for Loops in Vue With These Useful Tips Image

Unlocking the Power of v-for Loops in Vue With These Useful Tips

Focus & Code Diff in Nuxt Content Code Blocks Image

Focus & Code Diff in Nuxt Content Code Blocks

A Comprehensive Guide to Data Fetching in Nuxt 3 Image

A Comprehensive Guide to Data Fetching in Nuxt 3