Using Vue 3’s defineAsyncComponent
feature lets us lazy load components. This means that they’re only loaded from the server when they’re needed.
This is a great way to improve initial page loads as our app will be loaded in smaller chunks rather than having to load every single component when the page loads.
In this tutorial, we’ll learn all about defineAsyncComponent
and look at an example that defers the loading of a popup until it’s required by our app.
Okay – let’s get into it.
What is defineAsyncComponent
// SOURCE: https://v3.vuejs.org/guide/component-dynamic-async.html
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '<div>I am async!</div>',
})
})
)
defineAsyncComponent accepts a factory function that returns a Promise. This Promise should resolve
when we successfully get the component from the server and reject
if something goes wrong.
To use it, we have to import it from Vue and then we can use it in the rest of script.
We can also easily add Vue components from other files using an import
inside of our factory function.
import { defineAsyncComponent } from 'vue'
// simple usage
const LoginPopup = defineAsyncComponent(() =>
import('./components/LoginPopup.vue')
)
This is the simplest way to use defineAsyncComponent
, but we can also pass in a complete options object that configures several more advanced parameters.
// with options
const AsyncPopup = defineAsyncComponent({
loader: () => import('./LoginPopup.vue'),
loadingComponent: LoadingComponent /* shows while loading */,
errorComponent: ErrorComponent /* shows if there's an error */,
delay: 1000 /* delay in ms before showing loading component */,
timeout: 3000 /* timeout after this many ms */,
})
Personally, I find myself using that first, shorter syntax more often and it works for most of my use cases, but it’s entirely up to you.
And it’s really that simple, so let’s get into our example.
Lazy Loading a Popup Component with defineAsyncComponent
For this example, we’re going to be working with a login popup that’s triggered by a button click.
We don’t need our app to load this component whenever our app loads becauseit’s only needed when the user performs a specific action.
So here’s what our login component looks like, it just creates a popup by blacking out the rest of the screen with position: fixed
and has a few inputs and a submit button.
<template>
<div class="popup">
<div class="content">
<h4>Login to your account</h4>
<input type="text" placeholder="Email" />
<input type="password" placeholder="Password" />
<button>Log in</button>
</div>
</div>
</template>
<script></script>
<style scoped>
.popup {
position: fixed;
width: 100%;
top: 0;
left: 0;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
}
.content {
min-width: 200px;
width: 30%;
background: #fff;
height: 200px;
padding: 10px;
border-radius: 5px;
}
input[type="text"], input[type="password"] {
border: 0;
outline: 0;
border-bottom: 1px solid #eee;
width: 80%;
margin: 0 auto;
font-size: 0.5em;
}
button {
border: 0;
margin-top: 50px;
background-color:#8e44ad;
color: #fff;
padding: 5px 10px;
font-size: 0.5em;
}
</style>
Instead of importing it and including it in our components options like we usually would…
<template>
<!-- "Standard" way of doing things -->
<button @click="show = true">Login</button>
<login-popup v-if="show" />
</template>
<script>
import LoginPopup from './components/LoginPopup.vue'
export default {
components: { LoginPopup },
data() {
return {
show: false,
}
},
}
</script>
We can instead use defineAsyncComponent
to only load it when it’s required (meaning the button is clicked and our v-if
is toggled)
<template>
<!-- Use defineAsyncComponent -->
<button @click="show = true">Login</button>
<login-popup v-if="show" />
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
LoginPopup: defineAsyncComponent(() =>
import('./components/LoginPopup.vue')
),
},
data() {
return {
show: false,
}
},
}
</script>
While this may look the same when we use our app, let’s Inspect Element > Network
to understand this small, yet important difference.
If we don’t use defineAsyncComponent
, as soon as our page loads, we’ll see that our app is getting LoginPopup.vue
from our server.
While in this example, it may not make the biggest performance issue, it still slows down the load a little bit and if we have dozens of components doing this, it can really add up.
However, if we look at the same tab using defineAsyncComponent
, we’ll notice that when our page loads, LoginPopup.vue
is nowhere to be seen. This is because it hasn’t been loaded yet.
But once we click our button and tell our app to show our popup, that’s when it’s loaded from the server and we can see it in the Network
tab.
This helps us achieve the best performance.
We only want to load the components needed on our page’s initial load. Components that are conditionally rendered are often not required when our page loads, so why make our app load them in?
How to use with an asynchronous setup function
Regardless if we defer loading with defineAsyncComponent
, any component with an asynchronous setup function must be wrapped with a <Suspense>
.
Let’s take a look at an example. This is from ourIntroduction to Suspense Components – which is a great resource if you’re new to async setup functions.
In short, creating an async setup function is one option we have to make our component wait for some API call or other asynchronous action before rendering.
Here is our component with an async setup. It mimics an API call with a setTimeout()
<template>
<div class="popup">
<div class="content">
<p>Loaded API: {{ article }}</p>
<h4>Login to your account</h4>
<input type="text" placeholder="Email" />
<input type="password" placeholder="Password" />
<button>Log in</button>
</div>
</div>
</template>
<script>
const getArticleInfo = async () => {
// wait 3 seconds to mimic API call
await new Promise((resolve) => setTimeout(resolve, 1000))
const article = {
title: 'My Vue 3 Article',
author: 'Matt Maribojoc',
}
return article
}
export default {
async setup() {
const article = await getArticleInfo()
console.log(article)
return {
article,
}
},
}
</script>
We can import it in our component with or without defineAsyncComponent
import LoginPopup from './components/LoginPopup.vue'
// OR
const LoginPopup = defineAsyncComponent(() =>
import('./components/LoginPopup.vue')
)
But if we want this to render inside of our template, we need to wrap it in a Suspense element. This waits for our setup function to resolve before attempting to render our component.
A neat feature of Suspense is that we can display fallback content using slots and templates. The fallback content will display until the setup function resolves and our component is ready to render.
Note that the v-if is moved from the component itself to our Suspense component so all fallback will display.
<template>
<button @click="show = true">Login</button>
<Suspense v-if="show">
<template #default>
<login-popup />
</template>
<template #fallback>
<p>Loading...</p>
</template>
</Suspense>
</template>
This is the result. A user will see “Loading…” and then after 3 seconds (the hard-coded value for our setTimeout
), our component will render.
By default, all components we define using defineAsyncComponent are suspensible.
This means if there is Suspense in a component’s parent chain, it’s treated as an async dependency of that Suspense. Our components loading, error, delay, and timeout options will be ignored and will be handled by Suspense instead.
Final Thoughts
defineAsyncComponent
can be beneficial when creating large projects with dozens of components. When we get to lazy load components, we can have faster page load times improving the user experience and eventually increasing retention and conversion rates on your application.
I’d love to know your thoughts on this feature. If you’re already using it in your apps, let me know how in the comments down below!
But until next time, happy coding.