Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RTK x100 slower than vanilla Redux at average (up to x400) #4793

Open
gentlee opened this issue Dec 28, 2024 · 3 comments
Open

RTK x100 slower than vanilla Redux at average (up to x400) #4793

gentlee opened this issue Dec 28, 2024 · 3 comments

Comments

@gentlee
Copy link

gentlee commented Dec 28, 2024

I've wrote some benchmarks trying to understand how Immer influences the redux performance and got some very strange results:

  • the worst case is RTK slower than vanilla Redux by 408 times, and at average it is slower by 100 times...
  • even using immutable approach with original doesn't make much difference.

There are probably some mistakes in this code (initially generated by AI), so appreciate some help to find that out.

Here is the code.

To run: NODE_ENV=production node --expose-gc benchmark.mjs

Environment:

  • Node v22.12.0
  • Mac M1 16Gb

Results:

Data, ms (the lower the better)
  Vanilla Immer Immer NoAutoFreeze Immer Immutable Immer Immutable NoAutoFreeze
Add Item 131.3 13450.59 11812.4 11857.97 26037.62
Remove Item 358.88 24837.13 23621.49 8038.03 7194.04
Update Item 423.92 15680.84 14181.17 9658.24 28933.6
Concat Array 120.51 49178.25 47117.22 10940.38 36005.91
[Average] 258.65 25786.7 24183.07 10123.66 24542.79
Data, x times slower (the lower the better)
  Vanilla Immer Immer NoAutoFreeze Immer Immutable Immer Immutable NoAutoFreeze
Add Item 1 102.44 89.96 90.31 198.31
Remove Item 1 69.21 65.82 22.4 20.05
Update Item 1 36.99 33.45 22.78 68.25
Concat Array 1 408.08 390.98 90.78 298.78
[Average] 1 99.7 93.5 39.14 94.89
Chart, x times slower

Image

PS.

  • Initially test array had 100,000 size, but decreased to 10,000 to be closer to the average apps. Results are for the second.
  • Immer means using RTK's configureStore and createSlice. Vanilla means using createStore from redux.
@phryneas
Copy link
Member

phryneas commented Dec 28, 2024

Hmm. This is admittedly slower than I would expect - I would expect an "add item" immer reducer here to take more around 2ms than 6ms. This might be worth looking into.

That said, the 100x/400x from your headline here are highly artificial and hopefully never occur in a real application, and that also goes into the whole benchmark a bit:

See the style guide in these points:

The idea here is that an action should represent an event happening in your application - in almost all situations, those are either user-induced events (clicks), or things like the start or return of a network request.
That kind of action happens usually less than once per second, although it might sometimes "cluster" to up to 10 times per second (e.g. if on application start a lot of inpendent network requests are made). I'd never expect it to happen more often than that in a typical application though.
In a typical application, the render coming after a store update would be more expensive than the store update, so you'd try to prevent too many actions in the first place.
In those environments, you might see one action that adds 5000 elements, but you wouldn't see 5000 actions that add one element each.

Of course there are exceptions to this - what comes to mind are cases where we've seen Chatbots processing messsages from hundreds of users, or financial dashboards showing real-time data coming in via websocket. These cases can be optimized, or in a single reducer you might actually choose to opt out of immer by just writing a reducer by hand - but these cases make up the 1% of poweruser edge cases, not the 90-95% of users we primarily target and for whom we want to avoid actual real-life bugs.
In these poweruser edge cases we expect that people take closer care, maybe follow immer's performance tips, opt out by writing a reducer by hand, or frankly: not use Redux. If you need the pinnacle of performance, even a plain reducer might be doing too much work, since you always have to immutable clone everything.
This might just be a use case where you would want to start mutating objects and sidestep a state manager.

That said - I already started with this: these numbers feel too high by a factor of 3-5 to what I'd expect, and it is worth investigating.

Could you maybe adjust the benchmarks to resemble real-life application usage a bit more?
Also, some remarks regarding the benchmark:

  • please use createStore or configureStore in all benchmarks - it doesn't really make sense to run some benchmarks through additional middleware, while omitting that for others
  • the output is a bit confusing - could you change it to console.log(${name}: ${((end - start)/runCount).toFixed(2)});?

@gentlee
Copy link
Author

gentlee commented Dec 29, 2024

About actions per second - I was once participated in a project that had around 100 actions per second - it was a fork of opensource mobile chat app Mattermost, they recently switched to watermelon db (bad solution). Also many projects got lots of actions per second on app launch when fetching lots of stuff.

Also, we need to keep in mind that there are mobile devices, and even more, android mobile devices (x10 slower than iOS), and even more old android mobile devices with low battery, so for a good app there should be a pretty big performance reserve for such cases, if comparing to desktop.

And actually redux does pretty well in terms of performance. I would say much better than I expected. So it has a great potential in performance, but looks like not with immer. It adds a huge limitations here, and that chatting app would definitely freeze out if someone decided to use it (they used vanilla redux back then), as it already had performance issues due to some very bad decisions like keeping theme in single redux store and many more.

So not using redux when it actually could work very well is not cool honestly. There probably are some situations where there can be better solutions, but it is definitely not a chatting app.

  • Adjusting the benchmarks to resemble real-life application: I think 10000 length for an array is a good starting point, 1000 would be too small. I would consider a good chatting app with infinite pagination as a default target. But currently don't have time to improve it further, may be later.
  • createStore removed. It showed pretty much the same performance as configureStore.
  • the output changed.

Also did some refactoring etc.

EDIT: As for making plain reducers - they don't have all this cool boilerplate of slices, so not sure why not just disable immer for them / for slice reducers.

Also, I guess there definitely should be a tip in performance doc about that.

@markerikson
Copy link
Collaborator

markerikson commented Dec 29, 2024

I don't understand what you're saying with this sentence:

As for making plain reducers - they don't have all this cool boilerplate of slices, so not sure why not just disable Immer for them / for slice reducers.

As Lenz said: Redux was never intended to be the fastest lib out there. The primary goal is predictability, and accidental mutations go against that.

Sure, I wish Immer was faster :) I can see that the majority of the time in your benchmark is being spent in Immer's finalizeProperty internal method:

Image

I know @mweststrate has spent a lot of time trying to optimize it, but also have it be correct in a variety of update situations. Maybe there's still something that could be done to speed it up, maybe there isn't. Might be worth filing a perf issue over in the Immer repo to discuss this.

So yeah, in cases where you're using Redux and perf is still critical, you may feel the need to hand-write those reducers.

But overall we still see Immer as the right default choice for RTK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants