[DRAFT] Prototype proxy-based shallow equality selector perf optimization #2246
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR:
useSyncExternalStoreWithSelector
method from theuse-sync-external-store
package and converts it to TSuseSelector
to use our inlined versionThe intent is to improve app-wide selector performance by skipping selectors that are unlikely to be producing a new result.
Background
React and Redux have always been "80%" solutions. They work fine in most cases, but they aren't the most optimized option perf-wise, and it takes work to squeeze out extra perf.
Redux is by definition an O(n) subscription setup. N selected components means N subscriber callbacks and selectors run on every dispatch. This is fine with hundreds of connected components, but as it gets into the thousands, this can become noticeable overhead.
Auto-Tracking Experiment
2 years ago I very briefly played with the idea of trying to Proxy-wrap selectors - first in Reselect, then in React-Redux:
That was a single 4-hour in-flight hacking session. I reused a bunch of "auto-tracking" code from the Ember world. The idea was:
My thought was that we'd ship some opt-in approach (param for
<Provider>
or an alternate impl, plus a specificuseTrackedSelector
hook), to avoid the runtime and bundle overhead unless you wanted it.The bigger question was whether the cost of doing that tracking + reconciliation would be less than the cost of skipping the subscribers.
My quick hack session got something sorta-kinda working and some tests passing, but also looked like the perf cost of doing that work could be awfully expensive.
More Research
Since then I've kept an eye out for signals libraries that looked like they might be useful (and still have several ideas I want to play with there at some point).
However, I also saw a relevant PR earlier this year:
This directly changed
useSyncExternalStoreWithSelector
to do shallow root field comparisons, rather than trying to do deep nested tracking.Based on some conversations I've had with folks who had very complex Redux apps (20K+ connected components), it sounded like just bailing out of irrelevant first-level state changes could be a decent bang for the buck.
That PR got closed, so I figured I'd try porting the changes to React-Redux to try them out.
Status
At the moment, all our existing tests pass. I've run some local checks on the existing https://github.com/reduxjs/react-redux-benchmarks using the new version, and it seems generally equivalent so far. At least it's not worse, but also not sure it's actually helping improve that much.
I'll have to do some more perf testing and get a better sense of whether it actually is helping or not.
Not sure what a final productionized form might look like. I'm not terribly keen on either fully forking
uSESWS
, or flat-out changinguseSelector
to use the new behavior all the time. I could imagine it being a new hook instead, so you'd have to explicitly import it and use it intentionally. It's also entirely possible this is just a questionable line of research to start with, and not worth shipping.