Optimize the Performance of a Vue App with Async Components
Want to learn Vue.js from the ground up? Get an entire collection of Vue books covering fundamentals, projects, tips and tools & more with SitePoint Premium. Join now for just $14.99/month.
Single-page applications sometimes cop a little flack for their slow initial load. This is because traditionally, the server will send a large bundle of JavaScript to the client, which must be downloaded and parsed before anything is displayed on the screen. As you can imagine, as your app grows in size, this can become more and more problematic.
Luckily, when building a Vue application using Vue CLI (which uses webpack under the hood), there are a number of measures one can take to counteract this. In this article, I’ll demonstrate how make use of both asynchronous components and webpack’s code-splitting functionality to load in parts of the page after the app’s initial render. This will keep the initial load time to a minimum and give your app a snappier feel.
To follow this tutorial, you need a basic understanding of Vue.js and optionally Node.js.
Async Components
Before we dive into creating asynchronous components, let’s take a look at how we normally load a component. To do so, we’ll use a very simple message component:
<!-- Message.vue -->
<template>
<h1>New message!</h1>
</template>
Now that we’ve created our component, let’s load it into our App.vue
file and display it. We can just import the component and add it to the components option so we can use it in our template:
<!-- App.vue -->
<template>
<div>
<message></message>
</div>
</template>
<script>
import Message from "./Message";
export default {
components: {
Message
}
};
</script>
But what happens now? The Message
component will be loaded whenever the application is loaded, so it’s included in the initial load.
This might not sound like a huge problem for a simple app, but consider something more complex like a web store. Imagine that a user adds items to a basket, then wants to check out, so clicks the checkout button which renders a box with all details of the selected items. Using the above method, this checkout box will be included in the initial bundle, although we only need the component when the user clicks the checkout button. It’s even possible that the user navigates through the website without ever clicking the checkout button, meaning that it doesn’t make sense to waste resources on loading this potentially unused component.
To improve the efficiency of the application, we can combine both lazy loading and code splitting techniques.
Lazy loading is all about delaying the initial load of a component. You can see lazy loading in action on sites like medium.com, where the images are loaded in just before they’re required. This is useful, as we don’t have to waste resources loading all the images for a particular post up front, as the reader might skip the article halfway down.
The code splitting feature webpack provides allows you to split your code into various bundles that can then be loaded on demand or in parallel at a later point in time. It can be used to load specific pieces of code only when they’re required or used.
Dynamic Imports
Luckily, Vue caters for this scenario using something called dynamic imports. This feature introduces a new function-like form of import that will return a Promise containing the requested (Vue) component. As the import is a function receiving a string, we can do powerful things like loading modules using expressions. Dynamic imports have been available in Chrome since version 61. More information about them can be found on the Google Developers website.
The code splitting is taken care of by bundlers like webpack, Rollup or Parcel, which understand the dynamic import syntax and create a separate file for each dynamically imported module. We’ll see this later on in our console’s network tab. But first, let’s take a look at the difference between a static and dynamic import:
// static import
import Message from "./Message";
// dynamic import
import("./Message").then(Message => {
// Message module is available here...
});
Now, let’s apply this knowledge to our Message
component, and we’ll get an App.vue
component that looks like this:
<!-- App.vue -->
<template>
<div>
<message></message>
</div>
</template>
<script>
import Message from "./Message";
export default {
components: {
Message: () => import("./Message")
}
};
</script>
As you can see, the import()
function will resolve a Promise that returns the component, meaning that we’ve successfully loaded our component asynchronously. If you take a look in your devtools’ network tab, you’ll notice a file called 0.js
that contains your asynchronous component.
Conditionally Loading Async Components
Now that we have a handle on asynchronous components, let’s truly harvest their power by only loading them when they’re really needed. In the previous section of this article, I explained the use case of a checkout box that’s only loaded when the user hits the checkout button. Let’s build that out.
Project Setup
If you don’t have Vue CLI installed, you should grab that now:
npm i -g @vue/cli
Next, use the CLI to create a new project, selecting the default preset when prompted:
vue create my-store
Change into the project directory, then install the ant-design-vue library, which we’ll be using for styling:
cd my-store
npm i ant-design-vue
Next, import the Ant Design library in src/main.js
:
import 'ant-design-vue/dist/antd.css'
Finally, create two new components in src/comonents
, Checkout.vue
and Items.vue
:
touch src/components/{Checkout.vue,Items.vue}
Making the Store View
Open up src/App.vue
and replace the code there with the following:
<template>
<div id="app">
<h1>{{ msg }}</h1>
<items></items>
</div>
</template>
<script>
import items from "./components/Items"
export default {
components: {
items
},
name: 'app',
data () {
return {
msg: 'My Fancy T-Shirt Store'
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
There’s nothing fancy going on here. All we’re doing is displaying a message and rendering an <items>
component.
Next, open up src/components/Items.vue
and add the following code:
<template>
<div>
<div style="padding: 20px;">
<Row :gutter="16">
<Col :span="24" style="padding:5px">
<Icon type="shopping-cart" style="margin-right:5px"/>{{shoppingList.length}} item(s)
<Button @click="show = true" id="checkout">Checkout</Button>
</Col>
</Row>
</div>
<div v-if="show">
<Row :gutter="16" style="margin:0 400px 50px 400px">
<checkout v-bind:shoppingList="shoppingList"></checkout>
</Row>
</div>
<div style="background-color: #ececec; padding: 20px;">
<Row :gutter="16">
<Col :span="6" v-for="(item, key) in items" v-bind:key="key" style="padding:5px">
<Card v-bind:title="item.msg" v-bind:key="key">
<Button type="primary" @click="addItem(key)">Buy ${{item.price}}</Button>
</Card>
</Col>
</Row>
</div>
</div>
</template>
<script>
import { Card, Col, Row, Button, Icon } from 'ant-design-vue';
export default {
methods: {
addItem (key) {
if(!this.shoppingList.includes(key)) {
this.shoppingList.push(key);
}
}
},
components: {
Card, Col, Row, Button, Icon,
checkout: () => import('./Checkout')
},
data: () => ({
items: [
{ msg: 'First Product', price: 9.99 },
{ msg: 'Second Product', price: 19.99 },
{ msg: 'Third Product', price: 15.00 },
{ msg: 'Fancy Shirt', price: 137.00 },
{ msg: 'More Fancy', price: 109.99 },
{ msg: 'Extreme', price: 3.00 },
{ msg: 'Super Shirt', price: 109.99 },
{ msg: 'Epic Shirt', price: 3.00 },
],
shoppingList: [],
show: false
})
}
</script>
<style>
#checkout {
background-color:#e55242;
color:white;
margin-left: 10px;
}
</style>
In this file, we’re displaying a shopping cart icon with the current amount of purchased items. The items themselves are pulled from an items
array, declared as a data property. If you click on an item’s Buy button, the addItem
method is called, which will push the item in question to a shoppingList
array. In turn, this will increment the cart’s total.
We’ve also added a Checkout button to the page, and this is where things start to get interesting:
<Button @click="show = true" id="checkout">Checkout</Button>
When a user clicks on this button, we’re setting a parameter show
to be true
. This true
value is very important for the purpose of conditionally loading our async component.
A few lines below, you can find a v-if
statement, which only displays the content of the <div>
when show
is set to true
. This <div>
tag contains the checkout component, which we only want to load when the user has hit the checkout button.
The checkout component is loaded asynchronously in the components
option in the <script>
section. The cool thing here is that we can even pass arguments to the component via the v-bind
statement. As you can see, it’s relatively easy to create conditional asynchronous components:
<div v-if="show">
<checkout v-bind:shoppingList="shoppingList"></checkout>
</div>
Let’s quickly add the code for the Checkout
component in src/components/Checkout.vue
:
<template>
<Card title="Checkout Items" key="checkout">
<p v-for="(k, i) in this.shoppingList" :key="i">
Item: {{items[Number(k)].msg}} for ${{items[Number(k)].price}}
</p>
</Card>
</template>
<script>
import { Card } from 'ant-design-vue';
export default {
props: ['shoppingList'],
components: {
Card
},
data: () => ({
items: [
{ msg: 'First Product', price: 9.99 },
{ msg: 'Second Product', price: 19.99 },
{ msg: 'Third Product', price: 15.00 },
{ msg: 'Fancy Shirt', price: 137.00 },
{ msg: 'More Fancy', price: 109.99 },
{ msg: 'Extreme', price: 3.00 },
{ msg: 'Super Shirt', price: 109.99 },
{ msg: 'Epic Shirt', price: 3.00 },
]
})
}
</script>
Here we’re looping over the props we receive as shoppingList
and outputting them to the screen.
You can run the app using the npm run serve
command. Then navigate to http://localhost:8080/. If all has gone according to plan, you should see something like what’s shown in the image below.
Try clicking around the store with your network tab open to assure yourself that the Checkout
component is only loaded when you click the Checkout button.
You can also find the code for this demo on GitHub.
Async with Loading and Error Component
It’s even possible to define a loading and/or error component for when the async component takes some time to load or fails to load. It can be useful to show a loading animation, but bear in mind this again slows down your application. An asynchronous component should be small and fast to load. Here’s an example:
const Message = () => ({
component: import("./Message"),
loading: LoadingAnimation,
error: ErrorComponent
});
Conclusion
Creating and implementing asynchronous components is very easy and should be part of your standard development routine. From a UX perspective, it’s important to reduce the initial load time as much as possible to maintain the user’s attention. Hopefully this tutorial has assisted you with loading your own components asynchronously and applying conditions to them to delay (lazy load) the load of the component.