Tell Vue.js to stop wasting time, and render faster!

Jason
7 min readSep 12, 2018

--

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.

A screenshot of the performance profiling tool running on the page.

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() .

Where our code gets data and passes it to leaflet.

Which then calls an internal leaflet function called_addLayer()

Leaflet internally managing layers; everything seems okay.

Which then creates an object holding our layer (GeoJSON object) and some other attributes.

Everything still seems okay

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:

Vue takes over when pushing a new item into an array.

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 wants to observe the new item pushed into the array.

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.

Vue checks if it has already installed an Observer.

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 core functionality of vue-nonreactive

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.

This is where Vue checks if it has already installed an Observer.

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.
    Our makeNonreactive function added this attribute so with the value.__ob__ = new Observer({}); line.
  • Second: Vue checks if value.__ob__ in an instance of Observer.
    Due to the const 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?

A capture of the performance timeline after making things “non responsive”.

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

--

--