500 ms to 5 ms
Sat, 10/14/2017 - 11:37

This is short example about Observables and how they can be really evil. Fortunately we have a hero called Object.freeze(obj).

Problem

Same as in previous article about Web Workers, we have large data sets, several objects that contain arrays of thousands values. This time it's not about processing, sorting, filtering these arrays, but just about storing it to vuex store. Structure looks like this:

// pseudocode
cars: {
  car1: {
    visible: Boolean(),
    metrics: Object(),
    trails: Array(30),
    values: Array(8000)[
      {available: Number, lat: String, lng: String},
      ...
    ]
  },
  ...
},
...

Execution of setter for cars state.cars = cars took around ~500 ms, which is quire a lot, when web becomes unresponsive for half second. And JS heap jumped to 270 MB, nobody wants that.

All observable

We can see, most of execution time is taken by reactiveSetter executed from setCars.What can I do with this? I know data won't change, it's loaded from API, and if it refreshes, whole car object gets refreshed. How to prevent those arrays to be observable?

Solution

It's simple, just get rid of those expensive Observables. You could just do this

commit('setCars', Object.freeze(cars))

All frozen

Now we are down from 500 ms to 0.15 ms, ~3000 times faster and with JS Heap dropped to ~67 MB. But there is little problem. We have key visible, that needs to be changeable. And that won't work with whole Object frozen.

TypeError: Cannot assign to read only property 'visible' of object '#<Object>'

So we freeze just parts of Object we need.

const frozenCars = {}
Object.keys(cars).forEach(key => {
  frozenCars[key] = {
    ...cars[key],
    metrics: Object.freeze(cars[key].metrics),
    trail: Object.freeze(cars[key].trail),
    values: Object.freeze(cars[key].values)
  }
})
commit('setCars', frozenCars)

Partially frozen

Ok, now we've added a bit of complexity, forEeach takes ~4 ms + 0.15 ms setCars, but still compared to 500 ms, we are good, 100 times faster and with interactive functionality.

Conclusion

Save time and memory with Object.freeze(obj), not everything needs to be observable.

Notes

You might wonder, why not to freeze data when we are transforming it (in Web Worker) and save those ~4 ms in forEach.

cars[car.metric.name] = {
  trail: Object.freeze(newCar.trail),
  visible: newCar.visible,
  metric: Object.freeze(newCar.metric),
  values: Object.freeze(newCar.values)
}

This would work nicely, if we had this on main thread. But we are sending data from worker to main with postMessage which uses Structured clone algorithm, so it would "unfreeze" objects.

You can find non-frozen code on github ra100/carshare-exporter-viz example/nofreeze branch.