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:
Use for...of with no array cloning for the notify-subscribers loop
Use .push() mutation when adding a subscriber (it will be called if the loop is currently iterating and that's fine)
Use .filter() when removing a subscriber to create a new array reference with the subscription removed. This new reference will be ignored on the current notify-subscriber loop (if any) and used next time.
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.
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:for...of
with no array cloning for the notify-subscribers loop.push()
mutation when adding a subscriber (it will be called if the loop is currently iterating and that's fine).filter()
when removing a subscriber to create a new array reference with the subscription removed. This new reference will be ignored on the current notify-subscriber loop (if any) and used next time.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 usingfor...of
on a normal array is the fastest way to iterate for the notify-subscribers loop and that's the more critical path.