### Introduction

By default, when writing a Vue.js Single Page Application (SPA), all necessary assets such as JavaScript and CSS files are loaded together when the page is loaded. When dealing with large files, this can lead to unsatisfactory user experience.

With the help of Webpack, it is possible to load pages on demand in Vue.js using the import() function instead of the import keyword.

A typical SPA in Vue.js works by having all the functionality and assets packaged and delivered together to allow users to use the application without the need to refresh pages. If you have not explicitly designed the application to load pages on demand, all the pages will be loaded either at once, or prefetched/preloaded ahead of time, using unnecessary bandwidth and slowing page loading.

This makes it especially bad for large SPA with many pages. People with slow internet connection or low end devices such as mobile phones would have a bad user experience. By loading on demand, users would never need to download more than they need.

Vue.js does not come with any loading indicator for dynamic modules. Even with prefetching and preloading - no visual indicator lets the users know how the loading is going. We'll also be adding a progress bar to improve user experience.

### Preparing the Project

First we need a way for our progress bar to communicate with the Vue Router. To do that, we will use the Event Bus Pattern.

The Event Bus is basically a singleton Vue instance. Since all Vue instances have an event system using $on and $emit, we can use it to pass events anywhere in our app.

Let's create a new file, eventHub.js in the components directory:

import Vue from 'vue'
export default new Vue()


Now, we'll configure Webpack to disable prefetching and preloading. We can either do this individually for each function, or disable it globally. Create a vue.config.js file at the root folder and add the configuration to disable prefetching and preloading:

module.exports = {
chainWebpack: (config) => {
config.plugins.delete('prefetch')
},
}


We'll be using Vue Router. To that end, we'll use npx to install it:

$npx vue add router  Now, let's edit our router file, typically located under router/index.js and update our routes to use the import() function, instead of the import statement: This is the default configuration: import About from '../views/About.vue' { path: '/about', name: 'About', component: About },  We've changed it to: { path: '/about', name: 'About', component: () => import('../views/About.vue') },  If you prefer to select which pages to load on demand instead of disabling prefetching and preloading globally, use the special Webpack comments instead of configuring Webpack in vue.config.js: import( /* webpackPrefetch: true */ /* webpackPreload: true */ '../views/About.vue' )  The main difference between import() and import is that ES modules loaded with import() are loaded at runtime while the ones loaded with import are loaded during compile time. This means we can defer the loading of modules with import() and load only when necessary. ### Implementing the Progress Bar Since it's impossible accurately estimate when the page will load (or if it'll load at all), we can't really make a progress bar. There's no way to check how much the page has loaded either. What we can do is create a progress bar that finishes when the page loads. Everything in between doesn't really reflect the progress, so in most cases, the progress depicted is just random jumps. Let's install lodash.random first, since we'll be using that package to select some random numbers during the progress bar generation: $ npm i lodash.random


Then, let's create a Vue component - components/ProgressBar.vue:

<template>
<div class="loader" :style="{ width: progress + '%' }">
<div class="light"></div>
</div>
<div class="glow"></div>
</div>
</template>


Now, to that component, we'll be adding a script. Within that script, we'll first import random and $eventHub, as we'll be using those: <script> import random from 'lodash.random' import$eventHub from '../components/eventHub'
</script>


Now, after the imports, in the same script, we can define some variables that'll we'll be using:

// Assume that loading will complete under this amount of time.
const defaultDuration = 8000
// How frequently to update
const defaultInterval = 1000
// 0 - 1. Add some variation to how much the bar will grow at each interval
const variation = 0.5
// 0 - 100. Where the progress bar should start from.
const startingPoint = 0
// Limiting how far the progress bar will get to before loading is complete
const endingPoint = 90


With those in place, let's write the logic to asynchronously load the components:

export default {
name: 'ProgressBar',

data: () => ({
isVisible: false, // Once animate finish, set display: none
progress: startingPoint,
timeoutId: undefined,
}),

mounted() {
$eventHub.$on('asyncComponentLoading', this.start)
$eventHub.$on('asyncComponentLoaded', this.stop)
},

methods: {
start() {
this.isVisible = true
this.progress = startingPoint
this.loop()
},

loop() {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
}
if (this.progress >= endingPoint) {
return
}
const size = (endingPoint - startingPoint) / (defaultDuration / defaultInterval)
const p = Math.round(this.progress + random(size * (1 - variation), size * (1 + variation)))
this.progress = Math.min(p, endingPoint)
this.timeoutId = setTimeout(
this.loop,
random(defaultInterval * (1 - variation), defaultInterval * (1 + variation))
)
},

stop() {
this.progress = 100
clearTimeout(this.timeoutId)
const self = this
setTimeout(() => {
self.isVisible = false
}
}, 200)
},
},
}


In the mounted() function you'll see that we're making use of the event bus to listen for asynchronous component loading. It will start the loading animation once the router tells us that we have navigated to a page that has not been loaded yet.

And finally, let's add some style to it:

<style scoped>
font-size: 0; /* remove space */
position: fixed;
top: 0;
left: 0;
height: 5px;
width: 100%;
opacity: 0;
display: none;
z-index: 100;
transition: opacity 200;
}

display: block;
}
opacity: 1;
}

background: #23d6d6;
display: inline-block;
height: 100%;
width: 50%;
overflow: hidden;
transition: 200 width ease-out;
}

float: right;
height: 100%;
width: 20%;
background-image: linear-gradient(to right, #23d6d6, #29ffff, #23d6d6);
}

.glow {
display: inline-block;
height: 100%;
width: 30px;
margin-left: -30px;
}

0% {
margin-right: 100%;
}
50% {
margin-right: 100%;
}
100% {
margin-right: -10%;
}
}
</style>


Now, let's add our ProgressBar to our App.vue or a layout component as long as it is in the same component as the router view. We want it to be available throughout the app's lifecycle:

<template>
<div>
<progress-bar></progress-bar>
<router-view></router-view>
</div>
</template>

<script>
import ProgressBar from './components/ProgressBar.vue'
export default {
components: { ProgressBar },
}
</script>


This all results in a sleek progress bar, looking like this:

### Trigger Progress Bar for Lazy-Loaded Pages

Our ProgressBar is listening on the Event Bus for the asynchronous component loading event. When something is loading this way, we'll want to trigger the animation. Let's add a route guard to the router to pick up these events:

import $eventHub from '../components/eventHub' router.beforeEach((to, from, next) => { if (typeof to.matched[0]?.components.default === 'function') {$eventHub.$emit('asyncComponentLoading', to) // Start progress bar } next() }) router.beforeResolve((to, from, next) => {$eventHub.\$emit('asyncComponentLoaded') // Stop progress bar
next()
})


To detect if the page is lazy loaded, we need to check if the component is defined as a dynamic import i.e. component: () => import('...') instead ofcomponent: MyComponent.

This is done with typeof to.matched[0]?.components.default === 'function'. Components that were loaded with import statement will not be classified as functions.