Vue.js App Performance Optimization: part 1 — Introduction to performance optimization and lazy…

Vue.js App Performance Optimization: part 1 — Introduction to performance optimization and lazy loading.

While mobile-first approach becomes a standard and uncertain network conditions are something we should always take into consideration it’s harder and harder to keep your application loading fast. In this series I’ll dig deep into Vue performance optimization techniques that we are using in Vue Storefront and that you can use in your Vue.js applications to make them loading instantly and perform smooth. My goal is to make this series a full and complete guide on Vue apps performance.

Part 1 — Introduction to performance optimization and lazy loading.

Part 2 — Lazy loading routes and vendor bundle anti-pattern.

Part 3— Lazy loading Vuex stores and single components — soon

Part 4— Lazy loading libs and finding smaller equivalents — soon

Part 5 — Making use of Service Worker cache — soon

Part 6— Prefetching

How Webpack bundling works?

Most of the tips in this series will focus on making our JS bundle smaller. To understand while it’s crucial first we need to understand how Webpack is bundling all of our files.

While bundling our assets Webpack is creating something called dependency graph. It’s a graph that links all of our files based on imports. Assuming we have a file called main.js specified as an entry point in our webpack config it will be a root of our dependency graph. Now every js module that we will import in this file will become it’s leaf in the graph and every module imported in this leafs will become their leafs.

Webpack is using this dependency graph to detect which files it should include in the output bundle. Output bundle is just a single (or multiple as we will see in the later parts) javascript file containing all modules from dependency graph.

We can illustrate this process like this:

Now when we know how bundling works it becomes obvious that the more our project will grow the biggest initial JavaScript bundle will be. The bigger it’ll be the longer it’ll take to download and parse it so user will see something meaningful later. The longer user will wait the more probable it is that he/she will leave our website.

In short words bigger bundle = less users. At least in most of the cases.

Lazy loading

So how we can cut off bundle size when we still need to add new features and improve our application? The answer is easy — lazy loading and code splitting.

As the name suggests lazy loading is loading parts of your application lazily. In other words — loading them only when we really need them. Code splitting is just splitting the app into this lazily loaded chunks.

In most cases you don’t need all the code from your Javascript bundle right after user visits your website. Even if we will have 3 different routes in our app no matter where user will end up he/she always needs to download, parse and execute bundle with all of them even though only one is needed. What a waste of time and energy!

Lazy loading allows us to split the bundle and serve only the needed parts so user is not wasting time to download and parse code that’ll not be used.

To see how much of the JavaScript code is actually used in our website we can go to the devtools -> cmd+shift+p -> type coverage -> hit ‘record’. Now we should be able to see how much of the downloaded code was actually used.

Everything marked as red is something that is not needed on current route and can be lazily loaded. If you are using source maps you can click on any file in this list and see which of it’s parts were not invoked. As we can see even vuejs.org has a huge room for improvement ;).

By lazy loading proper components and libraries we managed to cut off the bundle size of Vue Storefront by 60%!

Ok, we know what lazy loading is it’s and that it’s pretty useful 😉

It’s time to see how we can use it in our Vue.js application.

Dynamic imports

We can easily load some parts of our application lazily with webpack dynamic imports. Let’s see how they work and how they differ from regular imports.

If we will import JS module in a standard way like this:

// main.js
import ModuleA from './module_a.js'
ModuleA.doStuff()

It will be added as a leaf of a main.js in the dependency graph and bundled with it.

But what if we will need ModuleA only under certain circumstances like a response to the user interaction? Bundling this module with our initial bundle is a bad idea since it may not be needed at all. We need a way to tell our application when it should download this chunk of code.

This is where dynamic imports can help us! Now take a look at this example:

//main.js
const getModuleA = () => import('./module_a.js')
// invoked as a response to some user interaction
getModuleA()
  .then({ doStuff } => doStuff())

Let’s take a quick look at what happened here:

Instead of directly importing module_a.js we created a function that returns theimport() function . Now webpack will bundle content of dynamically imported module into a separate file and unless the function will be invoked the import will not be called and file won’t be downloaded. Later in the code we downloaded this optional chunk of code as a response to some certain user interaction (like route change or click).

By making a dynamic import we are basically cutting off the leaf that will be added to the dependency graph and making the separate one that will be downloaded when we decide it’s needed (which implies that we are also cutting off modules that are imported inside module_a.js ).

Let’s see another example that will better illustrate this mechanism.

Let’s assume we have 4 files: main.js, module_a.js, module_b.js and module_c.js. To understand how dynamic imports work we need only source code of main and module_a:

//main.js
import ModuleB from './mobile_b.js'
const getModuleA = () => import('./module_a.js')
getModuleA()
  .then({ doStuff } => doStuff()
)
//module_a.js
import ModuleC from './module_c.js'

By making module_a a dynamically imported module we are cutting part of the dependency graph with module_a and all it’s children. When module_a is dynamically imported it loads together with modules imported inside of it.

In other words we are just creating a new entry point for the dependency graph.

This how our dependency graph and bundles will look like with given setup.

Lazy loading Vue components

We know what lazy loading is and why we need it. It’s time to see how we can make use of it in our Vue application.

The good news is that it’s extremely easy and we can lazily load whole SFC along with it’s css and html with the same syntax as previously!

const lazyComponent = () => import('Component.vue')

…that’s all you need! Now the component will be downloaded only when it’s requested. Here are the most common ways to invoke dynamic loading of Vue component:

  • function with import is invoked
const lazyComponent = () => import('Component.vue')
lazyComponent()
  • component is requested to render
<template>
  <div> 
    <lazy-component />
  </div>
</template>
<script>
const lazyComponent = () => import('Component.vue')
export default {
  components: { lazyComponent }
}
</script>

Please note that the invocation of lazyComponent function will happen only when component is requested to render in a template.
For example this code:

<lazy-component v-if="false" />

will not dynamically import the component since it’s not added to the DOM (but will do it as soon as the value will change to true which is a nice way to conditionally lazily load Vue components)

Summary

Lazy loading is one of the best ways to make your web app more performant and cut off on it’s size. We learned how to use lazy loading with Vue components. In the next part of this series I’ll show you how to split your Vue application code with vue-router and async routes.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.