Or, “How to stop Vue.js doing lots of set up to watch data that will never change.”
The Backstory
Today, I was working on a page that displayed a map using Vue.js.
When loading the page, there was consistently a weird moment where the page stopped responding for a few seconds. I’m new to Vue.js, so I was pretty confident I was doing something wrong.
This isn’t highly related to the issue, but to give some context, I’m using Leaflet to render items on a map. At first I looked into using vue2-leaflet, but as the documentation links don’t point to any actual documentation, I decided against it. When you are working on a commercial product, using a library with no easily accessible documentation is rarely a good idea.
The Problem
Vue.js, like React, is fantastic at watching for changes in data. When your data changes, the framework updates your page efficiently, and everything is great. But what happens when you have a lot of data? Let’s say about 13 megabytes of nested arrays and objects that are not going to change, until maybe some later point where it will all be completely replaced.
How did I figure out the problem?
Performance Profiling! (I use Chrome, but Firefox has something similar).
I won’t go into detail about how to use the profiling tools in this post, but I’ll show you what to look for. The screenshot below shows a timeline of events happening after I loaded my page, and which functions are running at each moment in time. The red outlined areas in the image below are pointing out work that Vue is doing unnecessarily.
There are three AJAX requests here that are pulling data from the server during page load. But what is happening with all that “Observer” stuff? All three AJAX responses all seem to trigger Vue to do unnecessary observer setup, but the last one takes nearly 6 seconds.
The page is unresponsive during this time!
Observing and watching
Well, it turns out that Vue needs to closely watch each data blob for changes. In the majority of cases, this is exactly what you want, but to watch all that data, Vue adds observers
.
First, let’s think of a Vue observer
as a little tracking device. Imagine that Vue attaches these little tracking devices everywhere to the data it cares about to watch for changes.
This usually is very quick, perhaps only a few milliseconds in total, but when you have a lot of data, these milliseconds quickly add up as Vue has to add this to every single object.
Oh, and because of the way JavaScript works, while the observers are being added, the page can’t do anything else (eg. no scrolling), and will effectively seem frozen until completion.
Let’s see what Vue is actually doing
In the screenshot below, we’ve stopped our script running after receiving the AJAX response. some of this code is Leaflet
-specific and not 100% relevant to the actual problem, but this part of the code is where I encountered the issue.
So, we put the data into a GeoJSON layer using L.geoJson()
. After this is done, we pass that object to the Layer Controller, usinglayerControl.addOverlay()
.
Which then calls an internal leaflet
function called_addLayer()
Which then creates an object holding our layer (GeoJSON object) and some other attributes.
Now, this is where it starts to get interesting though. Leaflet calls this._layers.push
, which should simply add the new object to the end of the regular this._layers
array. But in the next screenshot, we see that we end up in Vue code:
How did we get to Vue code? Well, when Vue starts up, it attaches itself into common Array operations, so any time push
, pop
, shift
, unshift
, splice
, sort
, or reverse
is called on an array, Vue will be notified via ob.dep.notify()
.
This makes sense for most data, for example, items in a todo list. If you call reverse
on your todo list array, you would generally expect Vue to recognise this change, and reverse the list of items rendered on your page.
It’s important to note that when an item is pushed into array, before Vue will take any action based on that change, it ensures it has observers properly set up on that item, via a call to observeArray(inserted)
.
Vue iterates over all arguments given to Array.push()
, and calls observe()
.
Now, pay close attention to how Vue checks if it already is watching the object.
If there is already an attribute on an object called __ob__
and it’s the same type of object as Vue’s Observer
, then Vue knows there’s already an observer for that data.
The solution. AKA: Trick Vue into being less smart
The solution I ended up landing upon is quite simple. There’s no inbuilt Vue switch to say “No, don’t watch this”, but a library called “vue-nonreactive” exists and is here to save the day!
The library provides a handy function Vue.nonreactive(…)
which takes in your object and modifies it so Vue won’t watch it. It’s actually quite interesting how it does this, so that’s what the next section is going to discuss.
How Vue.nonreactive works
The package’s “How this works” section actually describes its functionality very well, but in case it went over your head, I’ll break it down and reword it here.
When Vue observes an object, it walks each attribute and converts it into a reactive property. Any nested objects are then also observed.
Simply put, this means Vue goes to every little corner of your data, and puts a tracker in each corner to make sure it notices all changes.
However, Vue will skip observation if it detects that the object is already observed.
Vue won’t put two trackers in the same spot. That would be unnecessary.
We can make an object non-reactive by assigning a dummy observer, duping Vue’s observer detection.
We can place a few fake trackers/observers in our data which trick Vue into thinking it’s already tracking everything, so it won’t spend time looking into all corners of your massive data blob.
How to stop vue installing observers
The vue-nonreactive
library can be simplified down into the following lines of code.
The makeNonreactive()
function in the image performs the same operation as calling Vue.nonreactive()
fromvue-nonreactive
, but has been extracted out for clarity.
The first line copies the constructor function of a Vue Observer
object, creates a dummy Observer
object, and installs it into __ob__
on the given argument.
That’s it. That’s the whole trick. But how does it work?
Let’s say we had run this code above on our data, as soon as we get it back from the AJAX request. Then, we can revisit the screenshot where Vue adds observers.
On the highlighted line, Vue checks two if the data has an observer by checking two things:
- First: Vue checks if the is an
__ob__
attribute.
OurmakeNonreactive
function added this attribute so with thevalue.__ob__ = new Observer({});
line. - Second: Vue checks if
value.__ob__
in an instance ofObserver
.
Due to theconst Observer = (new App()).$data.__ob__.constructor;
, line this also passes.
So by calling our simple makeNonreactive
function on our data, the if
condition passes, and we have successfully tricked Vue into thinking it already has proper observers installed.
At this point, if we hadn’t added a dummy “Observer” to __ob__
, then the if
condition would fail, and Vue would add a real observer (which includes a deep dive into the data, adding observers everywhere it can).
Are we done? Does this really fix the problem?
If we call this function (or use the vue-nonreactive
library) on our data as soon as we get it, everything should be fixed, right?
In the image above, we see the results of our change. Processing the third AJAX response starts just after 8000ms in the screenshot. This code was previously taking six seconds, but is now taking less than half a second!!
I call that a success. Hopefully you now have a better understanding about Vue.js observers and how to stop Vue doing unnecessary work.
Thanks for reading!
After almost 10 years of working full time as a software developer, this was my first technical blog post. Hopefully there are more to come in the future.
Jason