Introduction

We continue with our series on Lazy loading and Intersection observer API topic. In the previous article, we covered everything you need to know about the basics of the Intersection Observer API and touched a little on how to implement lazy loading.

If you want to read the previous article, follow this link: “Intersection observer is a hidden feature of JavaScript ”.

This article is a little different from the Part 1 because we will dive into creating a simple Vue app that utilizes the Intersection Observer API to implement lazy loading components in a more efficient way.

Until the end of the article, you will learn how to create a reusable lazy loading component that can be easily integrated in any Vue project, allowing for faster initial page load and improved user experience. As a bonus, we will make lazy loading nice and professional by adding a smooth transition as the component appears.

So, if you're interested in learning how to make your Vue app more performant with lazy loading components, then stick around!

If you are still learning Vue.js and you are eager to learn more, I recommend services like Codecademy where you can easily learn Vue, JavaScript, TypeScript, and other programming languages and frameworks. There is also a Basic plan where you don't need to spend any money.

Learn JavaScript for free with Codecademy

Creating a new Vue app

Since we already know about the Intersection Observer from Part 1, we can jump right into creating a fresh Vue app. To create a fresh Vite app with Vue, we need to run this command in the terminal:

yarn create vite lazy-loading-app --template vue

For people who use NPM instead of yarn, we can run this:

npm create vite@latest my-vue-app -- --template vue

If we were using TypeScript, which I always do, we would write vue-ts instead of vue in our command. Since this article is focused on Vue and Lazy loading, we will be sticking to JavaScript instead of TypeScript, but, I highly recommend using TypeScript.

Now that we have our fresh Vue app installation, we can open our project in the code editor and install all the necessary dependencies by running npm i or yarn.

Lastly, let's preview the project in the browser by running npm run dev or yarn dev and see everything running smoothly. This is what we should see in the browser:

Fresh Vue app previewed in the browser

Cleanup

Since we don't need lots of boilerplate code, we can remove some default Vue components that come with a fresh installation, such as HelloWorld.vue and styles.css, which can be found in the src directory.

Moreover, we need the App.vue to be empty like this:

<script setup>
</script>

<template>
</template>

<style scoped>
</style>

The main.js file has to be something like this:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

At this point, we only should have 2 files in the src directory, which are App.vue and main.js. Now we are ready to start.

What are we going to do?

To have the ability to reuse lazy loading functionality for as many components as we want, we need to create a component that acts as a wrapper for our lazy loading functionality. For this, we will utilize the Vue slots functionality, which allows us to pass content into our component and render it in specific places.

This is the final API of how we want to use lazy loading functionality:

<LazyLoading>
    <Posts />
</LazyLoading>

If we have a Posts component that fetches a list of blog posts from the server and displays them on the page. We can wrap it with a LazyLoading component to make it mount when a user scrolls down the page towards the component's viewport, thus reducing the initial loading of the page.

Creating a Lazy loading component

To create a Lazy loading component, we need to start by creating a new file in the src/components directory called LazyLoading.vue. This is where we will define all the logic with the Intersection Observer API.

Let's start with defining 2 variables:

<script setup>
import { ref } from 'vue'

const rootElement = ref(null)
const renderComponent = ref(false)
</script>

A rootElement variable is a ref that holds a reference to the DOM element that will be observed by Intersection Observer API. A renderComponent variable is just a boolean that determines whether the component should be rendered or not.

Next, I'll add the template:

<script setup>
import { ref } from 'vue'

const rootElement = ref(null)
const renderComponent = ref(false)
</script>

<template>
    <div ref="rootElement">
        <slot v-if="renderComponent" />
    </div>
</template>

The template is straightforward, it's simply a slot with a v-if directive that checks if the renderComponent variable is true, and if it is, it renders the content passed through the slot.

Now is the most important piece of logic, which is the Intersection Observer API implementation. Fortunately, it is simple:

function createIntersectionObserver() {
    return new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                renderComponent.value = true
            }
        }
    })
}

We are just returning the instance of the IntersectionObserver and looping through all the observed entries, checking if any of them are intersecting with the viewport. If an observed entry is intersecting with the viewport, we set renderComponent to true, which will render the component passed through the slot.

The only thing that we need to add is to execute this function when the component is mounted.

onMounted(() => {
    if (rootElement.value) {
        createIntersectionObserver().observe(rootElement.value)
    }
})

We call the createIntersectionObserver function to create an instance of IntersectionObserver and observe the rootElement using the observe method. That's it, we have implemented a lazy loading component using the Intersection Observer API in Vue. This is the whole component:

<script setup>
import { onMounted, ref } from 'vue'

const rootElement = ref(null)
const renderComponent = ref(false)

onMounted(() => {
    if (rootElement.value) {
        createIntersectionObserver().observe(rootElement.value)
    }
})

function createIntersectionObserver() {
    return new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                renderComponent.value = true
            }
        }
    })
}
</script>

<template>
    <div ref="rootElement">
        <slot v-if="renderComponent" />
    </div>
</template>

To test it, we need to have at least some component that has some logic. This is what we will do next.

Fetch posts component

We already know how to create and use lazy loading in Vue using the Intersection Observer API. In this section, we will create a simple component with minimum styles that makes AJAX requests and displays posts from the given API.

We need this component to illustrate a real-world scenario where you might have a Vue component that you want to be lazy-loaded to improve the performance of the page. There is no point in making 20 AJAX requests when the page loads if the user doesn't even scroll down to see them. Wouldn't it be better to defer the loading until the user actually needs to see them?

Let's start by creating a src/components/Posts.vue file with this content:

<script setup>
import { ref, onMounted } from 'vue'

const posts = ref([])
const loading = ref(true)

onMounted(fetchPosts)

function fetchPosts() {
    fetch('https://jsonplaceholder.typicode.com/posts')
        .then(resp => resp.json())
        .then(resp => posts.value = resp)
        .finally(() => loading.value = false)
}
</script>

<template>
    <h2 v-if="loading">Loading... Please wait...</h2>

    <div v-else class="wrap">
        <article v-for="post in posts" :key="post.title">
            <h2>{{ post.title }}</h2>
            <p>{{ post.body }}</p>
        </article>
    </div>
</template>

<style scoped>
.wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
}

article {
    background-color: aliceblue;
    border: 1px solid #deedfb;
    padding: 1rem;
    border-radius: 10px;
}
</style>

I'm not going to explain everything because it is as simple as it can get. This article is about Lazy loading and not about how to make AJAX requests. Let's display this component by adding it to the src/App.vue component.

<script setup>
import Posts from './components/Posts.vue'
</script>

<template>
    <main>
        <h1>Lazy loading with Vue</h1>
        <Posts />
    </main>
</template>

<style scoped>
main {
    max-width: 800px;
    margin: 0 auto;
    padding: 1rem;
}
</style>

After running yarn dev and going to http://localhost:5173 in my Firefox browser, this is what I see:

Blog posts rendered on the page with Vue.js

Let's push down the posts so that we cannot see them when we load this page. I will do it by adding lots of Lorem ipsum text between posts and the title.

<script setup>
import Posts from './components/Posts.vue'
</script>

<template>
    <main>
        <h1>Lazy loading with Vue</h1>

        <p>Lorem ipsum, dolor sit amet consectetur...</p>
        <p>Lorem ipsum dolor sit amet consectetur...</p>
        <p>Lorem ipsum dolor, sit amet consectetur...</p>
        <p>Lorem ipsum dolor sit amet consectetur...</p>

        <Posts />
    </main>
</template>

<style scoped>
main {
    max-width: 800px;
    margin: 0 auto;
    padding: 1rem;
}

p {
    font-size: 1.2rem;
}
</style>

I've truncated the paragraphs just to fit the text nicely on the web page for this article, in the real app I have long paragraphs.

When I preview the page in the browser, I get this:

Vue component with HTML markup rendred in the Firefox

Our posts are not visible on the screen, but they are still loaded in the DOM. Let's fix this by adding our newly created lazy loading component like this:

<template>
    <main>
        <h1>Lazy loading with Vue</h1>

        <p>Lorem ipsum, dolor sit amet consectetur...</p>
        <p>Lorem ipsum dolor sit amet consectetur...</p>
        <p>Lorem ipsum dolor, sit amet consectetur...</p>
        <p>Lorem ipsum dolor sit amet consectetur...</p>

        <LazyLoading>
            <Posts />
        </LazyLoading>
    </main>
</template>

We are done! When we open the developer tools in the browser and navigate to a “Network” tab, we can see that no AJAX requests were made. As soon as we scroll down to the bottom of the page, our posts appear magically.

Posts component being lazy loaded when user scrolls down the page

Smooth transition

To make the transition smoother, we can add a fade-in effect to our posts when they appear on the screen to make the user experience even better. Since Vue makes it easy to add transitions to our components, we can easily do that by creating a src/components/Transitions/AppearTransition.vue file in our project.

This is the content of the file:

<template>
    <transition name="smooth-appear">
        <slot />
    </transition>
</template>

<style scoped>
.smooth-appear-enter-active,
.smooth-appear-leave-active {
    transition: opacity .4s ease;
}

.smooth-appear-enter-from,
.smooth-appear-leave-to {
    opacity: 0;
}

.smooth-appear-enter-to,
.smooth-appear-leave-from {
    opacity: 1;
}
</style>

Go back to a LazyLoading.vue file and wrap our slot with our newly created AppearTransition.vue component like this:

<template>
    <div ref="rootElement">
        <AppearTransition>
            <slot v-if="renderComponent" />
        </AppearTransition>
    </div>
</template>

Don't forget to import the AppearTransition component at the top. If we test everything in our browser, we should see a nice transition effect when posts component is loaded on the screen.

If you don't see a transition, it's probably because the transition duration is 0.4 seconds, try to change it to 4 seconds, and you will see it.

Conclusion

In conclusion, lazy loading is a powerful technique that can significantly improve the performance of our Vue applications while also providing a better user experience and reducing the initial load time of web pages.

We have learned everything about the Intersection Observer API in the Part 1 of the Lazy loading series. In this part, we applied this knowledge to implement lazy loading and smooth transitions in Vue application.

In the next article, we will build a Lazy Loading component in Svelte that uses the Composition API to improve the performance and page loading speed of your app.

If you have any questions or want to add your thoughts, please feel free to leave a comment below, and I'll reply as soon as I can.

Keywords: frontend, js, smooth