Omnistac / zedux

:zap: A Molecular State Engine for React
https://Omnistac.github.io/zedux/
MIT License
344 stars 7 forks source link

feat(core): micro-optimize iterating and removing store subscribers #76

Closed bowheart closed 1 year ago

bowheart commented 1 year ago

Description

After lots of stress testing, one of Zedux's (relatively small) performance bottlenecks in very extreme cases is the createStore's cloning of the subscribers array on every dispatched action. This array is very small in Zedux stores since most "subscriptions" are handled by the atom graph.

We clone the subscribers to prevent mid-iteration mutation (deletion specifically) from causing a non-deleted subscriber to be skipped in the notify-subscribers loop. If we don't clone, then we'll have to prevent mutation by other subscribers. This means the .subscribe() method will have to either schedule its subscription to be added when notify-subscribers is over or it will have to replace the array ref, performing a clone itself, which will slow down subscription adding and deleting.

After lots of analysis, it turns out that cloning such a small array on delete is definitely better than cloning it in the notify-subscribers loop because we can use array.filter() which is very, very fast on such a small list. The optimal solution is:

This does mean that adding a subscriber during notify-subscribers behaves differently from deleting then adding a subscriber during notify-subscribers. The former case will call the new subscriber as part of the current notify-subscribers loop. The latter will push the new subscriber onto the new array created by the deletion's .filter(), meaning it won't be called during the current notify-subscribers loop.

I'm fine with this. Let's be honest, nobody is adding subscribers in other subscribers in the first place. I especially don't expect that this discrepancy could ever cause a bug, since you don't expect a subscriber to be called immediately anyway.

I put together a benchmark here directly comparing the possible deletion options when subscriber count is <= 10. This approach is by far the fastest (even compared to JavaScript's new .toSpliced()) on small lists. And even on large lists, its performance is respectable and very much offset by the fact that using for...of on a normal array is the fastest way to iterate for the notify-subscribers loop and that's the more critical path.