Object.freeze on large objects that don’t have to be reactive. If you want to know a bit more about why this works
and how big a performance gain it is, read on.
Naomi provides visualisations of HIV epidemic indicators across a country, at a fine-grained regional level. Users can filter the data by many parameters, e.g. age, sex, year, level of of detail (from the whole country down to individual cities.)
The front-end app is written in Vue.js + Vuex and for the map plots we are using the excellent Leaflet library. The HIV indicator data and GeoJSON are returned from an API and are in the order of 10MB in size; the fitering of data happens in the front-end and the map is redrawn each time the filtered data changes.
I used Chrome’s performance tools to see what was taking so long. The first tool I used was under the Performance tab.
record, perform the operations that you want to profile, in this case selecting data filters, then hit
stop to see a summary of time taken by events on the page.
I found the
Bottom-Up view most instructive - I could see that around 15% of the time was being taken
by a Vue function called
addDep, and that a function that just read a single value out of the data array was taking
over 100ms each time.
I used another Chrome tool to see how big the data was: the heap snapshot. You can find this under the memory tab in Chrome dev tools. In the results it was pretty easy to find the data array; it was the largest object in the heap with a retained size of 107MB! In this case the original data array coming back from the API was around 4MB. Looking at the items of the array, it became clear what was going on.
Under the hood
In Vue, everything is reactive by default. This feels like magic, but what’s really going on is that every object (in this case, every item in the array) is having an “observer” property added to it, this is an instance of the Dep class, and then having reactive getter and setter functions added for every property: https://github.com/vuejs/vue/blob/2.6/src/core/observer/index.js#L135 1
The upshot being that the 4MB array was growing by an order of magnitude.
In this case the fix was simple. There was actually no reason for the internal items of the array to be observable as
they are never mutated. In fact this is true of every sizable bit of data in the app - it is retrieved from the API
and effectively treated as readonly. The Vue documentation
points to the the solution - if an object is made readonly with
Object.freeze, then Vue can’t and doesn’t try to make
the object reactive.
Once we wrapped the API responses with
Object.freeze, the heap shrank dramatically and so did the rendering time! The
array is back down to 4MB in the heap:
Looking at the new performance profile,
addDep now only take 3% of the time, and that function that looked up an
item in the array now takes a much more reasonable 0.1ms. That’s 3 orders of magnitude less time.