Hello!
TL;DR This:
mobx.extendObservable(store.widget, {
"widgetId1": { title: "test" },
"widgetId2": { title: "test" },
// ...
"widgetId100": { title: "test" }
});
Hangs the browser for one second (!!!). How to update 100+ widgets efficiently?
First, thank the author of MobX for a nice project! I read a lot of docs (and some code) and didn't find the answer to the question I reached. I am using React components with mobx-react, and I want to optimize my application as much as possible.
I am creating a dashboard which has some widgets on it. Widgets updates comes from the server asynchronously in array, which should not be an issue for MobX.
Problem: React render method in @observering component triggers as many times as widget updates come from the server on the dashboard, even in single load (array), with some delay, which takes a lot of time.
I spent some time making an easy demonstration of my problem, see this pen demo.
From the demo, in short, I have the next observable store:
{
"dashboard": { // dashboard: { id: {DashboardData} }
"5820d47ad30dc546f224203a": {
"_id": "5820d47ad30dc546f224203a",
"widgets": [
"widgetId",
"widgetId0",
"widgetId1",
// ...
]
}
},
"widget": { // widget: { id: {WidgetData} }
"widgetId": { "_id": "widgetId", title: "Initial widget" },
"widgetId0": { "_id": "widgetId0" }, // no title came yet!
"widgetId1": { "_id": "widgetId1" }, // no title came yet!
// ...
}
}
Dashboards and widgets are stored without arrays to avoid crazy object nesting (user[] -> dashboard[] -> widget[] -> control[] -> and so on), and just for convenience (updates on different parts of UI).
Question: let's say my dashboard has 100 widgets. Within one HTTP request all 100 widgets come from the server in the array. How to update my store efficiently?
Because what I have done (demo) is crazy:

Over 1 second of JavaScript processing which hangs the browser. 100 re-renders of @observering component.
How can I achieve a better performance? What am I doing wrong with this simple store scheme? Are there any options to make extendObservable asynchronous and re-render React component only once?
Thank you very much.
Use observable.map() instead of plain object for store.widget
extendObservable is not intended to be used for dynamic data structures.
And don't forget to wrap the update into action/runInAction
EDIT: you may also want to use observable.shallowMap(), which doesn't convert values into observables. Alternatively (if you want them to be observable) you should update existing widget references instead of replacing them. In case the widgets themselfs have dynamic fields you want to represent them as maps as well...
Right, you need to run the said code in a transaction by wrapping it in an @action decorator. That would be 100 updates but only one render.
Thank you so much @urugator and @benjamingr, you saved my impatience in reading docs... Missed that points at all, I was not even expecting that they are implemented.
So i solved this by wrapping the update code with mobx.action and calling this action just right after declaration. Updated demo.
However, I see that now it takes 40-60ms (one/two frames) to update even with a single widget add. I close the issue but maybe you know how to avoid this ~50ms sync browser hang at all? This will make animations on the page a little bit buggy when data is coming. The first idea come to me is to get back to React's setState and use mobx.observe(model, "key", handler) to update the state.
Again...don't use extendObservable, use observable.map()/.shallowMap() and store.widget.set(id, widget) instead...
@urugator, @benjamingr, I've updated the pen with observable.map. Performance didn't change:

Still ~50ms of browser hang. Any ideas why render triggers synchronously? Or this is the way MobX @observer works and there's nothing to deal with here?
Thank you.
The pen you posted has only 2 widgets and I get ~3ms, after switching it to 100 ~30ms.
Any ideas why render triggers synchronously?
What do you mean?
Do you want to re-render individual widgets as they are being updated (N updates -> N renders) or update all 100 widgets at once (1 update -> 1 render)?
Currently your observer observes for the map, so it simply re-renders (with all it's children) once the map changes (once the outermost action ends <=> once all widgets are updated), therefore you see all widgets updated at once (synchronously?).
@urugator, I mean that it seems that MobX is doing something against React was created for.
Add a second argument to console.log to see what I mean (I've updated the pen).
console.log(`Update ends after ${ Date.now() - time }ms!`, document.querySelectorAll(".xblock")[1].children[0].textContent);
Console output after "data comes":
Update starts
Render
Update ends after 50ms! Loaded widget #0
Loaded widget #0 label is picked up by querySelector, right? Now think: the code inside setTimeout updates the DOM synchronously when React JS was created to update the DOM asynchronously, with component state management. If I have multiple model updates and all the components will render synchronously after each update, the DOM will become slow again.
Once the action ends, Mobx calls forceUpdate on <Root>. Thats pretty much the same as calling setState. When rendering or DOM update occurs depends entirely on React...
React can batch multiple forceUpdate/setState calls, but there is only single forceUpdate call, so...
@urugator, this is shocking news for me... Never thought React would do re-render synchronously.
So the problem is in React's setState even. Thank you very much for your time and experience share! :)
I think you are mixing things up a bit. In React there is the render step which builds the VirtualDOM and there is the diffing and reconciliation that happens after. Rendering the VirtualDOM is synchronous (until React Fiber) but then React can batch things and only updates the actual DOM (diffing and reconciliation) whenever it sees fit.
and only updates the actual DOM (diffing and reconciliation) whenever it sees fit.
Thanks @hccampos for your reply!
I went to try everything you all suggest here, and this time I tried to make a super-simple React timer, with setInterval triggering tick() each second (*no MobX, React-only):
tick () {
console.log("Start before", this.state.elapsed, document.querySelector(".time").textContent);
this.setState({ elapsed: Date.now() - this.state.start });
console.log("Start after", this.state.elapsed, document.querySelector(".time").textContent);
}
And the console output:

From here I draw two conclusions:
setState, being triggered f.e. from setInterval causes React to set state and call render method synchronously, as @urugator mentioned, this.state.elapsed gets updated.
Most helpful comment
Use
observable.map()instead of plain object forstore.widgetextendObservableis not intended to be used for dynamic data structures.And don't forget to wrap the update into
action/runInActionEDIT: you may also want to use
observable.shallowMap(), which doesn't convert values into observables. Alternatively (if you want them to be observable) you should update existing widget references instead of replacing them. In case the widgets themselfs have dynamic fields you want to represent them as maps as well...