When we develop a project for our clients, we should always have an eye on performance. The application can only be user-friendly if the speed of each interaction is good enough. The question of what the website actually looks like only arises when the user sees the page after it has been rendered. I am sure that you have closed many web pages even before you saw the first content because you did not want to wait until it was finally loaded. According to the Hubspot assessment, the pages with a loading time of 0 - 2 seconds have the best conversion rates. Google also calculated statistics on this topic using the Web Vitals: it is stated that this number should not exceed 2.5 seconds. So do not underestimate the performance.
Here are some VueJS performance tips that will help you avoid the problems. Think about the structure of your components before you start developing – in this case you will avoid duplication of work and organise your code and data management in the best way.
Let's imagine we are developing a web shop. We have the shopping bag with items inside and we need to display them in the list. VueJS gives us a convenient way to loop through the objects and add some HTML for each of the items – we just use v-for directive for it. The code snippet below does exactly that – the <li> tag, all possible nested elements and all the logic for it will be repeated for each item in the shopping bag. No problems with this solution, we are good to go.
But what if we need to render more complex logic within the same loop? What if we need an input field for each item to enter its amount? We also need a delete icon to be able to remove the item from the shopping cart. And our marketing team asks us to add some nice text in case a user orders more than 5 items.
In this case, the code is going to be as follows:
The problem here is that every time you change the quantity of a single element, the marketingMessage function is executed for all elements within the array (you can comment out the console to test it). Why is that?
The component instance always has a corresponding watcher instance that "tracks" all the properties mentioned in the rendering. In the example above, we used item.quantity as a dependency inside the template. When the setter of item.quantity is triggered (when a user changes its value), the component is going to be re-rendered to display the actual information. Since we use the marketing message as a method to get the text – it is going to be called for each item during the re-rendering. So VueJS always keeps an eye on all the properties we use in the template and updates the component if one of them has changed (very important to understand and remember this mechanism). More about VueJS reactivity here.
Okay, back to our example. Even if we talk about the difference in milliseconds in this particular case – it is still extra time to execute the function 3 times instead of 1. We should never execute the code we don't need. Very often we don't even know for sure how many elements will be in the loop, because the user will add them dynamically. So we can't predict whether it will still be fast enough or not. Another disadvantage is that you have to pass the item to the removeItem function, since there is no other way to specify exactly which item to delete.
What to do in this case? We need to find a way where VueJS doesn't track all the item quantities, but tracks them individually and runs the logic for a single one, one at a time. Therefore, we need to create a new ShoppingBagItem component and move the "repeated" code to this component:
What exactly has changed?
- We don't use the item.quantity as a reactive prop for the App component anymore. The App component has no reason to update anytime something is changed – all the reactive dependencies are now inside its child – ShoppingBagItem.
- ShoppingBagItem has an item.quantity as a reactive prop now: the component will be re-rendered when the value is changed. But in this case it is going to happen for this item only since ShoppingBagItem has its own encapsulation now: the data (item prop) and the methods are responsible only for this particular item.
- Since we no longer need to pass the item as a function argument, we use a computed property to generate the marketing message instead. The code inside this computed property will only re-evaluate when some of its reactive dependencies have changed – so in our case when quantity of the item has changed. It is much better for performance, since the computed has the previous value in cache so the code will only run if the value has changed. Here is an amazing article about Computed VS Methods and also an official explanation.
This way we not only gain performance, but also receive more control over the code, as each component only does its job. We can easily extend the logic for the element in the future, add more computed props to perhaps conditionally adjust the style – we will never multiply the response time with this approach.
UI component libraries
Some time ago I implemented a UI with a lot of content on the page. I used Vuetify as UI framework which is a very good and powerful tool I really like to work with. There is nothing wrong with this framework. But...
On one of the pages, v-for was used to loop through the elements. Of course, I created a separate Item component to increase the speed – just as I described above. But the page speed still was not good enough. I spent some time before I discovered VueJS development tools extension and a Performance tab in it. There you can see how much performance it takes to mount the entire component, but also to mount each Vuetify UI component you use (on the screenshot below I marked it red).
It became obvious that the reason for the slow performance of the page in this particular case was the use of Vuetify components, since they took too much time. We will test it together so we can see the difference. But before we do that I need to mention: the Vue Dev Tools are very useful, informative and they help a lot with finding the problem, but they might increase the mount time in general. Therefore, to be 100% accurate in measuring the results, we will use Chrome's built-in Performance Tab.
Okay, let's test it together creating some simple UI using Vuetify card component. I use the same App component structure we had, just with 100 items inside the shopping list so we could see the difference better:
The total mount time is 1504 ms.
Now let's create the same UI with pure HTML and CSS. You can do this faster if you copy the HTML generated by Vuetify into the template and also save the generated styling into your local CSS classes to be used for the template you have. You'll have to optimise the code a bit, but it still saves some time. So now we have pure HTML elements that are no longer connected to Vuetify in any way:
The total mount time is 638 ms, which is 2.36 times less than before with Vuetify.
Clear all the interval functions, custom listeners or third-party libraries
Normally, VueJS takes care of all the clean-up for us when we use "built-in" standard features. For example, you don't have to destroy a watcher when you leave the component – VueJS does that for you. But if you use custom event listeners, have a "modal window" component from a third-party library, or set an interval for fetching data, VueJS can't clean that up properly because those are not a built-in features, but your custom JS logic. In addition to performance issues (the logic is not removed from the call stack, so code continues to run with no need), it could lead to errors that are very difficult to find and fix. In those cases you need to use unmounted lifecycle hook to clean up the code. Here is a simple example of clean interval and remove event listener logic:
If your component tree is quite big, I assume you use the store to manage your data and share it between siblings. But sometimes we forget to think twice about whether we really need to store the data there instead of using a local state. Overuse of store, especially if you have a lot of two-way binding connections and store the status there, can lead to performance problems. Not only that – it is a bad architectural pattern in general and leads to more code and dependencies than necessary. This means: keep the local state as long as possible (possible means the code is readable and maintainable). Use it only if you feel that your project is so complex that you need help managing the state:
- You have a big components tree with a lot of grand-children, siblings to siblings or to parent communication;
- The data you are going to store has a complex flow, you use some getters and actions for it;
If you want to get more understanding what state management is, exactly, and how to use it in the best possible way, take a look at this LogRocket article.
So, today we've talked about list rendering, UI component libraries, store management, and code cleanup to avoid memory leaks. Of course, there is much more you can do to improve performance. Part II of this blog post will be about watchers and their huge impact on performance; lazy loading of components, routes, and images; virtual scrollers for large lists. Stay tuned, part II is coming soon!
Your job at codecentric?
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.