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.
found in
---> <Cards> at src/components/Cards.vue
Changing the :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.days and index.description. However, we aren't changing this data. We derive index from the cardInfo array.
> 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 cardInfo called 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.
- v-if and v-show
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.
- v-for
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.