Hello everyone, and welcome to the second part of our performance tips (if you missed the first one, you can find it here). We've already discussed how to work efficiently with lists, how UI component libraries can affect performance, when to use stores, and how to clean up the code before leaving a component. Today we're going to talk about watchers and how much they can affect performance. We'll explore when we could replace them with computed and how to use watchers properly to get the best out of VueJS without speed issues. We will also discuss how to lazily load components, routes, and images, and why virtual scrollers can be useful in some cases. So let's get started.
Watchers is a very powerful tool that gives us the reactivity we need. However, you need to be aware of the performance problems that can occur if you overuse it or don't use it in an efficient way. So let's look at the cases where we need watchers, when we can replace them with computed properties, and how we use them when we really need them.
Watchers VS Computed
Let's recall our code example from my previous article (in case you missed it, you can check out the first part of the performance tips here):
We have already used "computed" in that example to create the marketing message. In simple words: "computed" is a property that is recalculated when its dependencies change. In our case: if the item quantity was changed, the computed property marketingMessage will react to it and recalculate the returned value for us.
The second possible solution you can see below. It uses watcher to update the message:
As you can already see - it is more complicated. The watcher doesn't create any property - it watches the changes over a reactive property we already have. Our steps to implement the logic with watcher would be:
- First of all, we need to store the marketing message as data property to be able to use it in our template.
- After that, we create a watcher for the item (we need to know when it was changed to update the message). Inside the handler, we conditionally assign the new value for the message and VueJS updates the screen for us.
- To start the watcher when the component is being initialized, we need to use the immediate option. When the component is mounted, the item prop is not changed, so the watcher has no reason to execute the logic. This means that the marketing message will always have its initial default state ("Thank you for your order") until we change something about the item. Since we don't know for sure what item quantity we will receive, we can't adjust the default value, so the watcher must also be executed when the component is initialized.
When you set up the watcher, it runs a big amount of code to watch the changes and schedule it inside a Vue scheduler. Computed properties are cached, and only re-computed on reactive dependency changes. So they are much more efficient.
It makes sense to use computed if:
- You only want to change some component data based on other data changes: computed takes some data and returns the generated value you need based on that data.
- You need to do some simple data transformations before returning the value. No manipulation with the DOM.
- You want to listen for changes in more than one data property and perform some simple transofrmation depending on that. All the properties you use inside the computed will be listened to.
Changing the text in the template based on some changes to the props or adjusting the class list would be perfect use cases for computed.
It makes sense to use watchers if:
- You need to track some data changes and performe an action: request sending, DOM manipulation, etc.
- You need to compare the old and the new value of the changed data.
- You only want to track changes to one data property. You can't track changes for multiple properties at the same time, as in the case of computed.
To get a better feeling on why and when we should use computed or watcher, I also highly recommend this article.
By default, VueJS will track the changes on the top level of the watched object. That means, if we watch the whole shoppingBag array, the watcher will be executed only if the array itself has changed (e.g. some elements added or removed). It does not track the changes within the nested data - in our case, within each individual item. So if we watch the shoppingBag and change the quantity of an item, the watcher will not detect these changes. Vue introduces deep watcher for those cases. Here is a simple example of both default and deep behaviour:
A Deep Watcher is used to monitor changes for all item properties. That means for our small test application, we are tracking not only the changes of quantity, but also the changes of a name. No matter how many properties the nested item has - it will track all of them, and the performance will be lower the more items you have. Here is what VueJS itself says about deep option: " Deep watch requires traversing all nested properties in the watched object, and can be expensive when used on large data structures. Use it only when necessary and beware of the performance implications". If you think about it more thoroughly, there aren't that many use cases where you need to track absolutely everything. In our case, we would like to track the quantity only. How to do that? Luckily, VueJS also supports a dot-delimited path as the key which is exactly what we need:
As you can see, we only watch the quantity, so changing all other possible nested properties will have no effect on the speed.
Lazy loading for routes
If the application is large, the size of the bundle is also large. But no matter how many pages we have - we don't display them all at once. In other words, it makes more sense to import the components only when they are needed for the page you are navigating to. Vue router documentation gives us an excellent explanation and also a small example of how to do it. The changes you need to make are minor, but they have a very positive impact on performance.
Lazy loading for components
In our shopping bag example, we have a delete button. Before user removes an item, it would be a good idea to ask if he/she really wants to do it. A modal window is usually used for such purposes. Since a modal window can be very useful in different parts of the application, it makes sense to create a separate component for it, rather than implementing it directly inside the ShoppingBagItem. VueJS offers us several ways to import a component:
- The "standard" method (the first screenshot): we import a child component when we initialize the parent component.
- The second one (the second screenshot) is when we import the child component only when we need it, using the defineAsyncComponent function (there is a difference between Vue2 and Vue3 implementation, please have a look at the linked information to learn more). The ModalWindow here is only imported when the user clicks the delete button (use v-if only since v-show hides the content, but still imports it from the beginning). It could be that our user doesn't use the delete functionality at all, so why should we import code unnecessarily. Everything, which is not required on the parent initial render should be imported using this way. In this article you can see in action how much this affects the performance.
Lazy loading for images
If user interface includes many images that are not visible during the first rendering (you have to scroll to see them), it is also useful to implement "lazy loading" for images. Images are large and can cause performance slowdowns very quickly.
There are several ways to implement "lazy loading" for images. For example, Vuetify offers the v-lazy component, which allows you to load the nested content only when it becomes visible. Super simple, productive and usable not for images only. You can find a lot of plugins which help you to lazy load the images as well (the most popular one is vue-lazyload). Also, I saw several examples of "native" lazy loading implementation using the power of intersection observers (here you can do a deep dive into it). You have to choose the option that is suitable for your specific case. Always keep an eye on how large the images are and how they can be rendered most efficiently.
It is important to mention virtual scrollers as well. Sometimes you need to display a long list of items on the screen without pagination. It makes no sense to render them all at once: this lowers the performance and just doesn't make any sense, as the user can only see the amount of elements that fit on the screen. To optimise this, you can use a virtual scroller.
There are some good plugins for this and they all basically do the same thing - display only the elements that should be on the screen at the moment, if user scrolls - display the next part and so on. I personally like this scroller because it has a solid sponsors list and was invented by the Guillaume Chau who is the member of the VueJS core team.
As we talk a lot about Vuetify in this article, I must also mention v-virtual-scroll - the scroller component from Vuetify. It supports dynamic height, scrolling vertically and is a good alternative to pagination or other special plugins.
And a few more tips to finish up
If you are reading this, you are almost there! Thank you for your time and patience 👍🏼. Last but not least, I would just like to remind you of the general rule of thumb: only use the third-party libraries that you really need to use. Check the performance tab from time to time to see how the new code affects the speed. And try not to over-engineer your code: there's strength in simplicity.
Happy and performative coding to you!
Your job at codecentric?
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.