Vue.js — What are async components and how to use them?
Async components are special components in a Vue.js application that are loaded asynchronously, meaning they are fetched from the server only when needed, rather than being included in the initial page load. This approach can lower costs by reducing unnecessary network requests, significantly improve page loading time, therefore enhancing user experience, and potentially increasing the conversion rate.
What components could be loaded asynchronously to meet those goals?
Large or complex components, such as sliders rendered at the bottom of the page, which are not critical for the initial view but may be needed later.
Components that are rendered based on user interactions such as pop-ups and modal windows.
Components used for features that are accessed less frequently or by fewer users, like social media widgets.
Practical usage of Vue.js async components
Let's create simple delete confirmation modal window for the purpose of this blog:
AppModal.vue
<script setup lang="ts">
import { useSlots } from "vue";
const slots = useSlots();
</script>
<template>
<div class="backdrop">
<div class="modal">
<div v-if="slots.header" class="header">
<slot name="header" />
</div>
<div v-if="slots.body" class="body">
<slot name="body" />
</div>
<div v-if="slots.footer" class="footer">
<slot name="footer" />
</div>
</div>
</div>
</template>
<style scoped>
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.03);
z-index: 10;
}
.modal {
width: 100%;
width: 320px;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 16px;
}
.header {
width: 100%;
padding: 16px 24px;
border-bottom: 1px solid #e6e6e6;
background: #fafafa;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.body {
width: 100%;
padding: 16px 24px;
}
.footer {
width: 100%;
padding: 16px 24px;
}
</style>
DeleteConfirmationModal.vue
<script setup lang="ts">
import AppModal from "./AppModal.vue";
const emit = defineEmits<{
cancel: [];
delete: [];
}>();
function handleCancel() {
emit("cancel");
}
function handleDelete() {
emit("delete");
}
</script>
<template>
<AppModal>
<template #header>
<p class="heading">Delete Confirmation</p>
</template>
<template #body>
Are you sure you want to delete this resource? This cannot be undone.
</template>
<template #footer>
<div class="controls">
<button @click="handleCancel" class="cancel">Cancel</button>
<button @click="handleDelete" class="delete">Delete</button>
</div>
</template>
</AppModal>
</template>
<style scoped>
.heading {
font-weight: 600;
}
.controls {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.cancel,
.delete {
border: none;
background: none;
cursor: pointer;
padding: 12px 24px;
border-radius: 12px;
font-weight: 700;
}
.delete {
background: red;
color: #fff;
}
</style>
App.vue
<script setup lang="ts">
import { ref } from "vue";
import DeleteConfirmationModal from "./DeleteConfirmationModal.vue";
const deleteModalOpen = ref(false);
const resourceExists = ref(true);
function handleDelete() {
deleteModalOpen.value = true;
}
function handleCancel() {
deleteModalOpen.value = false;
}
function handleDeleteConfirmation() {
resourceExists.value = false;
deleteModalOpen.value = false;
}
</script>
<template>
<DeleteConfirmationModal
v-if="deleteModalOpen"
@cancel="handleCancel"
@delete="handleDeleteConfirmation"
/>
<div class="resources">
<div v-if="resourceExists" class="resource">
<p>Important resource</p>
<button class="delete" @click="handleDelete">Delete</button>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
<style scoped>
.delete {
border: none;
background: none;
cursor: pointer;
background: red;
color: #fff;
padding: 8px 12px;
border-radius: 10px;
font-weight: 700;
}
.resources {
width: 100%;
height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
}
.resource {
display: flex;
gap: 16px;
align-items: center;
font-weight: 500;
}
</style>
With the above code, we receive the following result:
Jumping straight to the Network tab in the DevTools, we can notice that our modal window is downloaded during the initial page load, even if it's not needed, which may result in users waiting longer to see the desired content:
To solve this problem, async components become handy. We can utilize them with the defineAsyncComponent function, which Vue.js provides us out-of-the-box:
import { defineAsyncComponent } from "vue";
const DeleteConfirmationModal = defineAsyncComponent(
() => import("./DeleteConfirmationModal.vue")
);
The function accepts a loader function that returns a promise, which is exactly what the ES module dynamic import is. We can make use of it to achieve asynchronous loading in our application:
App.vue
<script setup lang="ts">
import { ref, defineAsyncComponent } from "vue";
const DeleteConfirmationModal = defineAsyncComponent(
() => import("./DeleteConfirmationModal.vue")
);
const deleteModalOpen = ref(false);
const resourceExists = ref(true);
function handleDelete() {
deleteModalOpen.value = true;
}
function handleCancel() {
deleteModalOpen.value = false;
}
function handleDeleteConfirmation() {
resourceExists.value = false;
deleteModalOpen.value = false;
}
</script>
<template>
<DeleteConfirmationModal
v-if="deleteModalOpen"
@cancel="handleCancel"
@delete="handleDeleteConfirmation"
/>
<div class="resources">
<div v-if="resourceExists" class="resource">
<p>Important resource</p>
<button class="delete" @click="handleDelete">Delete</button>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
<style scoped>
.delete {
border: none;
background: none;
cursor: pointer;
background: red;
color: #fff;
padding: 8px 12px;
border-radius: 10px;
font-weight: 700;
}
.resources {
width: 100%;
height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
}
.resource {
display: flex;
gap: 16px;
align-items: center;
font-weight: 500;
}
</style>
After the above code update, we can notice in the DevTools that the modal is no longer downloaded during the initial page load:
The modal component, along with all required dependencies, is downloaded after the user explicitly requests it – in this scenario, by clicking on the Delete button:
This approach makes our application more performant by enabling faster initial page loads and also saves unnecessary network requests, potentially reducing costs, as a percentage of users will never decide to use the deletion feature.
How not to use Vue.js async components?
Async components are awesome; however, if not used carefully, they might actually worsen the application user experience and potentially decrease the conversion rate. See what happens when a component critical for the initial view is made asynchronous, and the website is accessed by a user with a slow network:
Some of the content loaded immediately, while another part loaded with a delay, causing distraction and making the user uncertain of their location, therefore increasing the chance that they will leave and never return.
Conclusion
Async components in a Vue.js application play a crucial role in optimizing performance. They ensure that unnecessary components aren't loaded upfront, minimizing unnecessary network requests, potentially lowering business costs, and reducing the time users spend waiting for content to appear. It's important to note that they still have their weaknesses, and the responsibility is on the developer to use them properly to increase the value of the product.
The code used in this blog is available on GitHub.