mobx-state-tree: Orders of magnitude slower than mobx

Hi I’m migrating an app from mobx to mobx-state-tree and I’m facing some performance issues.

Here is the simplest example case:

https://mattiamanzati.github.io/mobx-state-tree-playground/#src=import { observable } from ‘mobx’ import { types } from “mobx-state-tree” const MSTStore %3D types.model({ items%3A types.optional(types.array(types.string)%2C [])%0A%7D).actions(self%20%3D%3E%20(%7B%0A%20%20%20%20setItems(items)%7B%0A%20%20%20%20%20%20%20%20self.items%20%3D%20items%0A%20%20%20%20%7D%0A%7D))%0A%0Aclass%20MobxStore%20%7B%0A%20%20%20%20constructor()%7B%0A%20%20%20%20%20%20%20%20this.items%20%3D%20observable(%5B%5D)%20%0A%20%20%20%20%7D%0A%20%20%20%20setItems(items)%7B%0A%20%20%20%20%20%20%20%20this.items%20%3D%20items%0A%20%20%20%20%7D%0A%7D%20%0A%0Aconst%20items%20%3D%20Array.from(Array(100000).keys()).map(x%20%3D%3E%20%60Item%20%24%7Bx%7D%60)%0A%0Aconst%20mstStore%20%3D%20MSTStore.create()%0A%0Aconsole.time(%22mst%22)%0AmstStore.setItems(items)%0Aconsole.timeEnd(%22mst%22)%0A%0Aconst%20mobxStore%20%3D%20new%20MobxStore()%0A%0Aconsole.time(%22mobx%22)%0AmobxStore.setItems(items)%0Aconsole.timeEnd(%22mobx%22)

Replacing an observable array with 100.000 items takes ~5sec when using MST and just 0.02sec when using mobx. The worst part is that it completely blocks the UI while doing so. Is there something I could do to improve speed?

EDIT: Running with NODE_ENV=production does not improve things too much.

development
mst: 744.248291015625ms
mobx: 0.052001953125ms
production
mst: 645.447021484375ms
mobx: 0.064208984375ms

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 11
  • Comments: 64 (35 by maintainers)

Most upvoted comments

Thanks to #810 / MST 3 will be significant faster, closing this issue for now. Best open a new one if follow up is needed after releasing MST 3

@mweststrate when using just frozen you will loose MST built-in functionality like references, views, and deserialization (in relation to references).

Also I am not sure if I understand what you mean with data points. Is a spreadsheet with cells something one would do with non-frozen types in MST?

Let’s consider a simple 100x100 spreadsheet in MST and Mobx and compare the performance with arguably the exact same functionality:

Prepare some test data:

const data: any = {};
for (let x = 0; x < 100; x++) {
    for (let y = 0; y < 100; y++) {
        data[x + "," + y] = { x, y };
    }
}

MST:

const MSTCell = types.model('Cell', {
    x: types.number,
    y: types.number,
    value: types.optional(types.string, '')
}).views(self => ({
    get cor() {
        return self.x + "," + self.y;
    },
    get columnName() {
        return this.x; // (for example number to alphabetic name)
    }
})).actions(self => ({
    setValue(value: string) {
        self.value = value;
    }
}));

const MSTSpreadsheet = types.model('Spreadsheet', {
    cells: types.optional(types.map(MSTCell), {})
});

console.time("Initializing MST spreadsheet...");
MSTSpreadsheet.create({
    cells: data
});
console.timeEnd("Initializing MST spreadsheet...");

// Takes about 2 seconds

Mobx:

class MobxCell {

    public x: number;
    public y: number;
    @observable public value = '';

    @computed get cor() {
        return this.x + "," + this.y;
    }

    get columnName() {
        return this.x; // (for example number to alphabetic name)
    }

    @action setValue(value: string) {
        this.value = value;
    }
}

class MobxSpreadsheet {

    @observable public cells: Map<string, MobxCell> = new Map();

    public constructor(data: any) {
        for (let i in data) {
            if (data.hasOwnProperty(i))
                this.cells.set(i, Object.assign(new MobxCell(), data[i]));
        }
    }

}

console.time("Initializing mobx spreadsheet...");
const spreadsheet = new MobxSpreadsheet(data);
console.timeEnd("Initializing mobx spreadsheet...");

// Takes around 0.24 seconds

The above MST takes a whopping 2 seconds to initialize. Things can only get worse from there as you add more cells / make the Cell model more complex. While having almost the same functionality in place we have to take a 80-90% performance loss when moving from mobx to MST.

Performance PR’s are welcome!

Note there are quite some // optimization comments in the code which indicate clear possibilities for optimizations. Without really testing I think the following things might help big time:

    1. Node is quite a complicated and generic object, but for immutable values (primitives) we should probably create a cheaper and much version of it (ImmutableNode) that doesn’t support attaching onPatch / onSnapshot handlers etc etc. This one might not be trivial btw
    1. Caching middleware listeners will probably make action invocations significantly cheaper (now they are collected on every call). Make sure to invalidate when adding listeners or moving nodes around
    1. Skipping freeze, at least in prod mode, by @skellock sounds like a good idea
    1. I think more properties of nodes can be cached (@computed), like root, environment etc.
    1. nodes have a lot of object allocations which are just empty arrays etc. This could be initialized as null instead of [], that makes object creation cheaper but requires a bit more pre-condition checking in the code. Shouldn’t be too complicated either

Here’s my latest implementation, now running in production. Everything seems to be working so far.

import { extras } from 'mobx'
import { types } from 'mobx-state-tree'

export default subType => {
  const BlankType = types.model({})
  class LazyType extends BlankType.constructor {
    constructor (opts) {
      super(opts)
      let instantiate = this.instantiate.bind(this)
      this.instantiate = (parent, subpath, environment, value) => {
        let node = instantiate(parent, subpath, environment, {})
        node._value = value
        return node
      }
      this.finalizeNewInstance = () => {}
      this.createNewInstance = () => ({})

      this.getChildNode = (node, key) => {
        this.initNode(node).getChildNode(key)
      }
    }

    initNode (node) {
      let instance

      let cd = extras.getGlobalState().computationDepth
      extras.getGlobalState().computationDepth = 0
      instance = subType.instantiate(
        node.parent,
        node.subpath,
        node.environment,
        node._value || {}
      )
      node.storedValue = instance.storedValue
      node.type = subType
      extras.getGlobalState().computationDepth = cd

      return instance
    }

    getValue (node) {
      return this.initNode(node).storedValue
    }

    getSnapshot (node) {
      return node._value
    }

    isValidSnapshot (value, context) {
      return subType.isValidSnapshot(value, context)
    }
  }
  return new LazyType({ name: subType.name })
}

@mweststrate @mattiamanzati

I’ve been toying around with lazy init-ing MST nodes as my app started to take over 10s to initialize my MST store of nested hell and I came up with a hacky solution to my problem – don’t instantiate the node until something actually cares about its value.

This is still all synchronous and brought me back to the 100s of ms load time. It’s making me wonder whether this would be a better default for MST.

An array with 50000 items with a single string property:

(full) init: 2976.90185546875ms
(full) read all items: 76.2509765625ms

(lazy) init: 140.288818359375ms
(lazy) read all items: 1957.93408203125ms

Code Sandbox

Would love to hear from you as to how to achieve this properly! Thanks!!

@mweststrate is there any plan to support storing actions on the prototype for MST in the future, or would this not be possible due to the way that MST works? Alternatively, would you recommend just using a single action function that takes a sub-action name as an argument and call other statically-defined functions to reduce the number of closures per object instance?

@mweststrate how are you getting a 6x bump? I’m seeing 15-25% improvement

[Dev] Initializing MST spreadsheet…: 743.10107421875ms [Prod] Initializing MST spreadsheet…: 645.926025390625ms

I randomly tried the sandbox today

this is the result

Creating sample data...: 0.5999999993946403ms 
10000
Initializing MST spreadsheet...: 0.19999999494757503ms 
Initializing mobx spreadsheet...: 0.29999999969732016ms

What I confused of are the dependencies.

it is stated

mobx 3.5.1
mobx-state-tree 1.2.1

I thought the performance was improved by the update of MST. But it seems that version 1 already performed

@skellock Even in production mode. Even when I don’t render a FlatList 😉

@mweststrate while wrapping all actions is helpful, some might easily do without it (if it affects performance dramatically). Maybe make this behaviour optional/configurable instead?

I am little shocked to see the difference in performance between mobx and mobx-state-tree. I just transformed the models of a mobx application to mobx-state-tree only to find out I can’t actually use it as it has to load thousands of objects in memory and I’m looking at > 10 sec boot time.

Mobx and mobx-state-tree can’t be that much of difference until you actually use mobx-state-tree’s functionality (when it should kick-in).

Would it make sense to opt-out some features of mobx-state-tree to make it more like basic mobx usage? For example, if a model is not likely to be changed I don’t need patches etc. .

One could argue that you wouldn’t need mobx-state-tree for these models but I would like to define my models in a simular way + I really like the way mobx-state-tree handles deserialization of json input.

@dnakov made a good example of lazy loading. However, if one were to loop trough the models for the initial view (for example to do some filtering), then the lazyloading wouldn’t make much of a difference.

If the performance penalty during initialization is because of support for the mutation functionality of the objects then would there be a way to enhance the objects as soon as a first mutation is being performed?

@mshibl it creates a new type that is a lazy version of the given type.

const Store = types.model({
  myActiveProp: MyActiveType
  myLazyProp: lazy(MyActiveType)
})

Another possible improvement would be having a computed value based on parent signaling if any of the parent have any listener for onPatch/onSnapshot or onAction, and don’t bubble up that event at all if any of that is not present! 😃

Yeah, that was already a TODO in the code, that would be a starting point for performance improvements for sure, node.ts and mst-operations.ts contains a lot of those perf todos in comments 😃

https://github.com/mobxjs/mobx-state-tree/blob/master/src/core/node.ts#L269

Yeah, I’d opt to not freeze at all in prod mode 😃

^ I’m with my fellow React Native brethern, Sanket here. I’d love to help out in anyway.

One very corner-case thing I’ve noticed when using frozen is the act of freezing objects seems to incur a lot of overhead. Especially on Android devices.

I know of other libs that opt out on prod builds.

I’d love to see some kind of types.yolo() that would be frozen minus the freeze. Or preferably, some kind of switch to turn off freeze/isFrozen just in prod?