Tracking down performance pitfalls in Vue.js
The gamer kid in me has always had an undying urge to make video games. I recently got to indulge this urge when I attended my first Global Game Jam a few months ago. My team and I built a game called “ZeroDaysLeft” for the web as a single page application using Vue.js. The game had a green theme, we thought about the environmental impact capitalism has on the earth and wanted to put an informative spin on it. There aren’t many games made using the Vue.js. My team was a day late and after a literal game of rock-paper-scissors to pick our framework, we rushed to code it and at the end of the weekend got our game working. Locally, everything worked really well. Naturally, we were proud of our code-Frankenstein and wanted to share it with the world.
There was just one problem—when we built the app and queried the domain, it was such a memory hog. It barely worked and every machine that we tried to run it on would hang in processing hell—even my beefy Intel i7 processor-based system would crash. The game jam’s time constraints snapped us back to reality, and we decided to put aside our production performance problems so we could at least pitch a completed game on our devices. Like almost every “completed” project, we forgot about it the next day.
Except I couldn’t let it go. It kept bugging me. Was it Vue.js? Was it Netlify? Was it our hacky code? I had to find out.
Investigating the slowdown
I started out with a quick test using Lighthouse. Thankfully, Firefox has a browser add-on for that. This is what I got back.
89% isn’t bad. As a matter of fact, compared to a lot of widely used websites, it is decent. The test mentions potential problems like the speed index and first contentful and meaningful paints. Theoretically, addressing these would make the score higher but not necessarily improve the apps huge performance problem. We have a couple of image and audio assets, though neither is large enough to cause hanging. We could over optimize these already optimized assets, but that would probably not help us at all.
The test didn’t offer us any real insight into what might be causing this performance problem. At this point, I’m thinking “is it Vue?” I have no reason to think so, but I’d be foolish to not check. I check the console for the deployed site and it’s blank. Warnings aren’t usually displayed in production. When I do the same locally, I’m hit in the head by a couple of Vue warnings.
Like a majority of developers, my take on console warnings is that they’re just that—warnings and not errors—so my attention is better focused on something else. I hang onto the hope that doing away with these warnings could fix my production issues. I decided to go a little deeper into each and fix them.
All of these warnings came from a component I created to display options called
Cards.vue, so that component might need a lot of rewrites.
I decided to tackle these console warnings in order.
> [Vue warn]: Avoid using non-primitive value as key, use string/number value instead. found in ---> <Cards> at src/components/Cards.vue
Vue.js has a lot of directives that make using the framework more intuitive, like
v-for, which quickly renders an array as a list. When we use it, we need to have a
:key to enable efficient re-rendering of components. However, we were using an object as a key, which is a non-primitive value and therefore caused this error. I decided to use the
index.description as a new key, as it is a string and would make for more efficient re-rendering whenever the values change.
> [Vue warn]: Duplicate keys detected: '[object Object]'. This may cause an update error.
---> <Cards> at src/components/Cards.vue
:key to a string—
index.description—for the previous error fixes this duplicate key error. We can only write string types to the DOM, so when we passed an object to be rendered, it was converted to its string equivalent—i.e [object Object]—and because this was previously our key, every object, despite holding a different value, would be converted to [object Object], thus our duplicate key warning. Now that the key isn’t an object, the warning goes away. So yay for efficiency.
> [Vue warn]: You may have an infinite update loop in a component render function. found in ---> <Cards> at src/components/Cards.vue
For a very vague warning, this one seems to be the most important. Infinite loops are synonymous with memory drains. The message does not tell us what could be wrong. It does hint that it has something to do with the render function in the component. Perhaps with our hacky code, we are triggering non-stop updates and that is taking up so much compute power that it crashes the browser and then the device.
The warning at least tells us to check
Cards.vue, so my first thought is to check the reactive properties in the component as one could be causing the error. Reactive properties, when changed trigger a re-render.
We’re displaying data from
index.description. However, we aren’t changing this data. We derive
index from the
> v-for="index in cardInfo.sort(() => Math.random() - 0.7).slice(0,4)"
We use this block of code to randomly sort the elements in the array, and then get the first four elements to display as options for a player to pick. When a user clicks an option, the
effects() function is called and, besides making calculations for how an action affects the game state, it uses the splice prototype on
cardInfo to remove the top four elements.
As it goes with reactive properties like
cardInfo in a framework like Vue that utilizes a virtual DOM, whenever the value of the data property changes, a re-render is triggered. In our case, we’re changing it directly with the
sort() prototype then deleting elements only to sort them again. All this triggers “infinite” re-renders, thus the warning.
I decided to change the logic behind how the data was being filtered and stop the multiple changes to a reactive property,
cardInfo. I installed
lodash.shuffle and defined a computed property,
shuffledList(), that would create a copy of
list. I applied the shuffle operation to it and returned a “frozen” result, which would be sliced to have four cards displayed. We used
Object.freeze() which would make the object we’re returning immutable completely stopping all re-renders. Problem solved.
Getting tripped up by my framework
To be honest, when I began my investigation, I figured I would have to optimize a lot of my assets. It just goes to show how careful we have to be when using a lot of the framework abstractions—in Vue specifically, each directive has to be used right and only where necessary because they most definitely have trade offs.
It made me think about what else I was doing that could add unnecessary performance issues to my application. Most modern front-end frameworks abstract a lot and make it much easier for us to make applications for the web, it’s important to remember the underlying performance issues that might arise when we use things. I use Vue.js a lot and decided to explore some directives I use without even thinking about performance implications they might have on my application. Three very popular directives in particular stood out for me.
Both directives are used to conditionally render elements but work very differently behind the scenes and of course as a result of this, need to be used differently.
v-if does not render your component initially and only renders components when the condition is true. This means if you toggle the visibility of a component multiple times it will be re-rendered repeatedly. You don’t want to use this if you are changing the visibility of a component multiple times. It will affect your performance.
A good alternative is
v-show. This will render your component regardless using CSS, but will only make it visible depending on whether the condition is true or false. This method does have its drawbacks as it won’t defer rendering of non-essential components to a time when you actually need them on screen. This works well if your initial render isn’t heavy.
This directive is usually used to render lists from arrays. It has a special syntax in the form of
item in list, where
list is the source data array and item is an alias for the array element being iterated on. By default, Vue adds watchers on the source data array that triggers a re-render whenever a change occurs. This constant re-rendering can have an adverse effect on app performance. It’s important to think about utility, if you only want to visualize the objects then
Object.freeze() is a good solution and can drastically improve performance. It’s important to remember however that you will not be able to update the component or edit the object data.
Doing this and realizing that Lighthouse might check to see the apps performance that could possibly affect a user experience in a more direct way, I was left asking how to track application performance on servers. Do we leave it up to intuition and the assumption that developers know what they are doing and are using best practices? Regardless, this whole experience left me with a different perspective on single page application performance. Feel free to check out the project repository on GitHub or say hi to me on Twitter.
_Like a majority of developers, my take on console warnings is that they’re just that—warnings and not errors—so my attention is better focused on something else._
Really? I think (or at least hope) the majority reads the warnings. You can ignore some warnings, that’s o.k. (e.g. missing i18n or such), but always read them!
You could say that it is not the best from vue to label them as “warn”, although they show up as errors in the console (you see the red background color? ;))
I think alert fatigue definitely starts to set in with console warnings. Especially when working on older codebases- you start to see the same console warnings over and over again. Most of them mean nothing so you associate the yellow color with noise.
Its very rare that I found a console warning that I noticed and went “Hey I should go fix that!”. Then again, I rarely code with vue, so maybe its just something to get used to when using the framework.
“I decided to use the index.description as a new key, as it is a string and would make for more efficient re-rendering whenever the values change.”
I’m sorry, I don’t understand this explanation.
All my tables have one single primary key, that is called id. I can distinguish, because one is :key=”transaction.id” and the other is :key=”currency.id”
I can’t wrap my head around why would this cause an error on update, but I just started vue, and don’t know yet how the update is going to look like. Thank you.
When you specify a key on a v-for, that key is placed as an attribute on the html element. If you dropped your entire object in as the key, then your key is [Object object] instead of a simple string or integer value. YOU have your v-for correct, by specifying a simple property as the key (your object ID)
The OP was using his entire object, and made his more efficient by just using a string property from that object. (its also why he had duplicate keys – you should not have the issue when you use your ids, as long as your ids are unique)
If you choose to use a non-unique string property as the key, you can make it unique by appending the index or some other property to it:
v-for=”(obj, index) of myList” :key=”obj.stringProperty+index”
The key here will end up being “description2” or some other unique string literal combination.
Yours will handle updates and re-renders fine, as long as your key is unique (which is true as long as your ids are unique)