-
I am working out a performant canvas drawing library and I would like to use Mobx as its underlying change tracker. However Mobx's object creation performance is not good enough. I have about 10,000 scene objects to create on launch. Each scene object has 3~4 inner objects and 10+ fields. Creating 40,000 objects takes about 500ms on my machine (Macbook Pro 2019), which is unacceptable for us. My main idea is to move getters/setters to class prototype. My classes' fields are static enough. This will be 2~3x faster in my test. However I don't know if this approach brings additional problems maybe in edge cases. Could contributors please help me figure it out? Or is there any tips to make it even faster? Thank you very much. The code below works in Node.js environment with const Benchmark = require('benchmark')
const { makeObservable, observable, computed, action, autorun, $mobx, _getAdministration } = require('mobx')
class Plain {
constructor() {
this.num = 0
this.num1 = 0
this.num2 = 0
this.num3 = 0
this.num4 = 0
this.num5 = 0
this.num6 = 0
this.num7 = 0
this.num8 = 0
this.num9 = 0
}
get computed() {
return this.num + 1
}
method() {
this.num = 1
}
}
class MobxPlain {
constructor() {
this.num = 0
this.num1 = 0
this.num2 = 0
this.num3 = 0
this.num4 = 0
this.num5 = 0
this.num6 = 0
this.num7 = 0
this.num8 = 0
this.num9 = 0
makeObservable(this, {
num: observable.ref,
num1: observable.ref,
num2: observable.ref,
num3: observable.ref,
num4: observable.ref,
num5: observable.ref,
num6: observable.ref,
num7: observable.ref,
num8: observable.ref,
num9: observable.ref,
computed: computed,
method: action
})
}
get computed() {
return this.num + 1
}
method() {
this.num = 1
}
}
const ObservableObjectAdministration = Object.getPrototypeOf(_getAdministration(observable({}))).constructor
class MobxHacked {
constructor() {
// makeObservable has many checks, which is slower. Just manually create the adm.
this[$mobx] = new ObservableObjectAdministration(this, new Map(), 'MobxHacked')
// Skip Object.defineProperty to make it faster.
this[$mobx].values_.set('num', observable.box())
this[$mobx].values_.set('num1', observable.box())
this[$mobx].values_.set('num2', observable.box())
this[$mobx].values_.set('num3', observable.box())
this[$mobx].values_.set('num4', observable.box())
this[$mobx].values_.set('num5', observable.box())
this[$mobx].values_.set('num6', observable.box())
this[$mobx].values_.set('num7', observable.box())
this[$mobx].values_.set('num8', observable.box())
this[$mobx].values_.set('num9', observable.box())
this[$mobx].values_.set('computed', computed(() => this.num + 1))
this.num = 0
this.num1 = 0
this.num2 = 0
this.num3 = 0
this.num4 = 0
this.num5 = 0
this.num6 = 0
this.num7 = 0
this.num8 = 0
this.num9 = 0
}
get computed() { return this[$mobx].getObservablePropValue_('computed') }
get num() { return this[$mobx].getObservablePropValue_('num') }
set num(value) { this[$mobx].setObservablePropValue_('num', value) }
get num1() { return this[$mobx].getObservablePropValue_('num1') }
set num1(value) { this[$mobx].setObservablePropValue_('num1', value) }
get num2() { return this[$mobx].getObservablePropValue_('num2') }
set num2(value) { this[$mobx].setObservablePropValue_('num2', value) }
get num3() { return this[$mobx].getObservablePropValue_('num3') }
set num3(value) { this[$mobx].setObservablePropValue_('num3', value) }
get num4() { return this[$mobx].getObservablePropValue_('num4') }
set num4(value) { this[$mobx].setObservablePropValue_('num4', value) }
get num5() { return this[$mobx].getObservablePropValue_('num5') }
set num5(value) { this[$mobx].setObservablePropValue_('num5', value) }
get num6() { return this[$mobx].getObservablePropValue_('num6') }
set num6(value) { this[$mobx].setObservablePropValue_('num6', value) }
get num7() { return this[$mobx].getObservablePropValue_('num7') }
set num7(value) { this[$mobx].setObservablePropValue_('num7', value) }
get num8() { return this[$mobx].getObservablePropValue_('num8') }
set num8(value) { this[$mobx].setObservablePropValue_('num8', value) }
get num9() { return this[$mobx].getObservablePropValue_('num9') }
set num9(value) { this[$mobx].setObservablePropValue_('num9', value) }
method() {
this.num = 1
}
}
// The method is indeed an action located in its prorotype.
MobxHacked.prototype.method = action(MobxHacked.prototype.method)
// Seems fine. Prints 1, 2
const obj = new MobxHacked()
autorun(() => { console.log(obj.computed) })
obj.method()
// Additional benchmark
const suite = new Benchmark.Suite();
suite
.add('plain', () => new Plain())
.add('hacked', () => new MobxHacked())
.add('mobx', () => new MobxPlain())
.on('complete', () => {
suite.forEach(a => console.log(a.name, a.hz))
console.log('plain / hacked', suite[0].hz / suite[1].hz)
console.log('hacked / mobx', suite[1].hz / suite[2].hz)
})
.run() output: $ node bench.js
1
2
plain 899060017.5005143
hacked 272042.2635221889
mobx 96103.39744264686
plain / hacked 3304.8542011825425
hacked / mobx 2.830724727339008 Update 1This does not work using the official minified mobx build because all internal symbols are mangled. If I try very hard to get the symbol name (using more reflections and regex), the result will be about 20% better. $ node bench.js
1
2
plain 910772702.4277862
hacked 422705.49651075463
mobx 124899.33385843087
plain / hacked 2154.6270629216056
hacked / mobx 3.3843695034424837 If I replace the build-in $ node bench.js
1
2
plain 902528382.1992904
hacked 466736.74788798543
mobx 124102.74632624639
plain / hacked 1933.698999025234
hacked / mobx 3.760889760336236 |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
If you don't need to support utilities like: constructor() {
this._num = observable.box()
this._computed = computed(() => this.num + 1)
}
get num() {
return this._num.get();
}
set num(value) {
this._num.set(value);
}
get computed() {
this._computed.get();
} *
That's correct If you don't mind a bit worse UX (dunno if these objects are internal or exposed to consumers), work with these primitives (box/computed/action) directly (without hiding behind getters etc). |
Beta Was this translation helpful? Give feedback.
If you don't need to support utilities like:
isObservable
,isObservableProp
,observe
*,intercept
* just use the observable primitives for the individual fields, no need to "simulate" the administration object behind[$mobx]
symbol.*
observe
,intercept
is supportable on the level of individual atoms, egobserve(obj._num, listener)
.That's correct
If you don't mind a bit worse UX (dunno if these object…