Tips for building fast and light Vue.js SPA

Building Single Page Apps is quite common these days. Frontend frameworks come with tools and boilerplates that scaffold starter projects which are most of the time Single Page apps.

However, as your application evolves, you add more features and more pages, and consequentially your SPA becomes harder to manage. The application starts to load slower, the javascript of your app is parsed slower by the browsers and you may start questioning whether SPA was the right decision or not.

There are 3 main aspects when we talk about the performance of a Single Page app:

Code Performance

This is most of the times correlated with what your application does and how intense some of the tasks might be for a browser or device. Some apps might not care too much about code performance because they might only retrieve and display data, while others might have some processing or complex visuals that have a high impact on the performance. Code performance can be most of the times improved by the developers who build the application unless they rely on some packages that cause performance issues.

Perceived Performance

This aspect is correlated to how users perceive the performance of a website. Perceived performance is often associated with “how fast a website feels when it loads” rather than how fast it loads and these 2 things can be quite different. While the website can load very quickly, it is important that it feels smooth and fast. For example, loading images progressively even if this process is slower might be perceived as smoother and faster compared to having content jumping around or displaying spinners.

Code Size

Code size has an impact on performance. The more code you load, the slower your website loads and this is especially true for javascript. An interesting read on this topic is The Cost of Javascript which has a very good comparison between loading an image and some javascript of the same size. While those 2 download in the same amount of the time, the Javascript takes 3.5 more seconds to parse, compile and execute on a low end mobile device compared to the image. Perhaps, code size is one of the main factors that affects overall performance of a SPA simply because code size can grow very fast in a SPA without noticing.

Improving Performance of a Vue.js SPA

Vue.js is great, no doubt about it. The framework itself is light and performant compared to other alternatives, however this doesn’t mean it will build you a performant SPA out of the box.

So here are some tips and tricks to improve performance for each of those categories listed above:

Code Performance

Avoid complex logic based on watch, computed, update or beforeUpdate hooks

Having complex logic around these Vue.js blocks might end up with triggering extra component re-renders or render loops that can affect the end performance of your code.

Some common examples that can affect performance are:

- Setting data inside computed

- Emitting/setting multiple data properties inside watchers that might end up triggering that watcher again

- Setting data inside beforeUpdate hooks that might end up with re-rendering the component

- Mutating object/array props directly

Try to avoid these and rely on simpler and cleaner ways such as:

- Transforming your data in your upper component before passing it down so the child doesn’t have to rely on watchers

- Emitting events to notify parent components rather than mutating props if it’s easier

- Replacing computed with watcher in case you need to set data when another piece of data changes

Measure code performance with Chrome Dev tools. You can find a good guide on how to do that here

Try to isolate certain areas of your application that feel slow and see if there is any bottleneck on the code side that slows stuff down.

Test on a low end device or with a CPU slowdown in chrome dev tools

If your app runs well with a CPU slowdown it will run even better on a desktop or laptop. We often develop on high end machines and most of the times and everything feels smooth and fast but this is not the case when testing on a cheap smartphone.

Use Vuex with caution

There is a tendency to overuse vuex and put every api call in it. While this might be a little bit helpful, try using vuex only and only when you need it. The answer to putting something into Vuex can be based on this:

Do I need this piece of data in more than 2 non child-parent components ? Yes -> store in vuex. No -> Use local component data.

This is important because storing everything in Vuex creates an extra layer of complexity and sometimes may lead to certain performance issues like having big chunks of data in the store that are not relevant for many portions of the app.

Avoid shady third parties

Although this sounds very generalized, try to not use many third parties especially if they have a small scope. An interesting read on this topic is this article by David Gilbertson who says that small third parties introduce a lot of cost from learning curve to maintenance cost which in the end is the same or even more as creating your own solution. If you need a small component or piece of code, take the time to make it yourself. This will give you more control to fine tune it and maybe avoid performance issues from third parties which can be hard to tackle.

Perceived Performance

Use GPU based transitions and animations

What does this exactly mean ? Well, try to avoid any transitions or animations which are not based on these properties:

  • Position — transform: translateX(n) translateY(n) translateZ(n);
  • Scale — transform: scale(n);
  • Rotation — transform: rotate(ndeg);
  • Opacity — opacity: n;

These css properties are very well optimized for all devices and will ensure smooth GPU based animations. This is especially important for mobile devices which, if don’t use animations based on these properties might make your website feel janky and slow.

Load content beforehand

If one of your pages needs to display a lot of content based on some data, you can try using `beforeRouteEnter` hook to retrieve your data. Consider this example:

Retrieving data in mounted hook:

Retrieving data in beforeRouteEnter hook:

Although first example is renders faster, it is not perceived as smooth as the second. Second example loads the page along with content rather than displaying the content later when data is received from the api. Here’s a comparison of the code for these 2 cases:

Fetch data in mounted/created hook

function getData() {
return butter.post.list({ page: 1, page_size: 10 });
}

export default {
data() {
return {
blogPosts: []
}
}
async mounted() {
let res = await getData();
this.blogPosts = res.data;
}
}

Fetch data in beforeRouteEnter hook:

function getData() {
return butter.post.list({ page: 1, page_size: 10 });
}

export default {
data() {
return {
blogPosts: []
}
}
async beforeRouteEnter(to, from, next) {
let res = await getData();
next(vm => {
vm.blogPosts = res.data;
});
}
}

You can see that the difference is not that big. The second option might feel a bit more verbose but it has a different visual impact.

Lazy load images

If your page contains a lot of images, it might be a good idea to lazy load them and display them with the help of api’s such as IntersectionObserver.

Here’s a quick way on how you could do that:

npm install vue-lazyload

import Vue from "vue";
import VueLazyload from "vue-lazyload";

Vue.use(VueLazyload);

Now for images, instead of src attribute for images use the v-lazy directive:

<img v-lazy="path-to-image">

Don’t use very long transitions and animations

Usually short animations feel snappier and faster. Having a really long animation is not worth it unless it’s a complex one that changes while being displayed (e.g loading animation)

Google Material Design suggests ranges between 200ms and 300ms This duration usually feels the best for animating dropdowns, menus, appearing content, page transitions and so on.

Also try not to overuse animations. People in the Vue.js community overuse transition and transition-group components because they are simple to use and you get excited about it but most of the time displaying a simple list without animations has better perceived performance especially if you’re on mobile and you see like 1 or 2 list items.

Code Size

Finally we get to our final topic. This one has many small tips that can have a big impact.

Consider small third party libraries or no libraries at all

For example moment.js can be replaced with date-fns which much smaller and tree shakeable.

Moment: you either import it all or nothing Date-fns: You can import only certain functions. The whole packages is 10 times smaller than moment.

I’d recommend Bundle Phobia which is a good resource to quickly measure how much a package weights. 
If you need a simple functionality, you can even consider making it yourself which would be smaller than importing a package that does 10 other extra things besides your needed functionality.

Use dynamic imports for routes

Using dynamic imports will make sure that the code for each page is loaded only when the user navigates to a certain page. It will also make sure that the initial code loaded when users first visit your website, will be smaller because it will contain only core packages used everywhere in the app.

Here’s how you can do it. Instead of this:

import Profile from '@/views/Profile';

Consider this:

const Profile = () => import('@/views/Profile');

If you’re question why you should split a route that has let’s say 3kb then you could make use of webpack chunk names to group more routes into a single file that is downloaded when users navigate to any of the routes placed in this chunk. Here’s an example:

const ClientDetails = () => import(/* webpackChunkName: "clients" */ 'src/views/admin/clients/ClientDetails');
const ClientList = () => import(/* webpackChunkName: "clients" */ 'src/views/admin/clients/ClientList');

Now we can say that we have a good reason to split these 2 pages into a separate chunk because they might have a bigger size. Another benefit of doing this is that certain packages that are used only in this pages will be bundled along with them without polluting the main vendor file.

Try doing this for ALL your routes. Yes, that’s right for all routes. This will result in a good performance boost.
One caveat when you do this will be that you might notice a slight delay when clicking on certain pages especially when you are on a slower connection. You can solve this UX issue by displaying a top progress similar yo how Youtube does it for example

Here’s the code to do it:

npm install nprogress

Create a progressbar.js file and place this code

import NProgress from 'nprogress';
const progressShowDelay = 100;
let routeResolved = false;
/**
* Initializes NProgress after a specified delay only if the route was not resolved yet.
*/
export function cancelTopProgress() {
routeResolved = true;
NProgress.done();
}
function tryInitProgress() {
routeResolved = false;
setTimeout(()=> {
if (!routeResolved) {
NProgress.start();
}
}, progressShowDelay);
}
export default function initProgress(router) {
router.afterEach(cancelTopProgress);
router.beforeEach((to, from, next) => {
tryInitProgress();
return next();
});
}

In your main.js or where you create your router, call initProgress with the vue router instance:

initProgress(router);

You'd also need some extra css which you can find here

Use only the packages you need

It's always tempting to add a new fancy package that does some cool stuff. Always take into consideration that this might affect your end app size.
For example if you build a custom solution, you might not need any css framework like Bootstrap, Bulma and so on. It's fairly trivial to create a grid or even create custom components for that. Even if it's more work, it will pay off in the end. For example, on a project we worked on fairly recently we were able to reduce the total loaded css from 150kb gzipped which was containing Bootstrap and some other stuff to only 20kb gzipby creating custom components for grid, layout cards and so on. A good inspiration can be the Element UI layout components which are open source. You can build your own layout components (row, col) which in the end can cover like 80% of your Bootstrap needs.

Optimize early and often

Try to optimize, enforce rules regarding packages and code size as early in the project as possible. Do it often. Make sure you check from time to time that these rules are respected and followed. This will ensure a small and performant app. Doing size optimizations at the end of the development phase can be very hard, can introduce a lot of bugs and it's more likely for you to give up just because you'd think it would take too much time.

Measure size

You can always check and enforce limits for your bundle size. If you use Vue CLI 3, it will output the size for each module. Make sure you always check it and fight with it so it doesn't go up. Above that, you can use tools like Lighthouse which is available in Chrome Devtools to measure size & performance. It will give you nice tips & hints on what you can improve.

Conclusion

I really hope that some of these tips, although general can help you build faster and lighter SPA Vue.js apps. These are only some of the best practices and tips to make an application small and performant. There are many more web related aspects which we didn’t cover like images, cache and more yet those can be discussed in a separate topic as they are not related to Vue so much and are general best practices for the web. Hope you enjoyed this article and found it useful. Feel free to leave a suggestion or comment below.


Originally published at binarcode.com.