You signed in with another tab or window.Reload to refresh your session.You signed out in another tab or window.Reload to refresh your session.You switched accounts on another tab or window.Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Track root-state-level selector field accesses via a Proxy
Upon updates, do comparisons of those fields in the old and new state to determine if any of the read fields changed
Only re-run the selector if a read field changed
The 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:
keep a single Proxy-wrapped root state copy at the top of the component tree, pass down in the subscription
any selectors accessing that force creation of nested auto-tracking for every touched field
when a store update happens, we do an immutable-reconciliation thingy with the state copy and mark fields as dirty
then by the time we run subscribers, we know if the fields they last read got updated or not, and could smartly bail out of even running the selectors
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 gotsomething 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 changeduseSyncExternalStoreWithSelector 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 existinghttps://github.com/reduxjs/react-redux-benchmarks using the new version, and it seems generally equivalent so far. At least it's notworse, 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 forkinguSESWS, 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.
Some related comments from a former Slack engineer I've talked to (stashing here because I have nowhere better to put this):
wanted to share some of the most promising stuff we've done
"key tracking": using defineProperty to track selector access to top level keys of state, then on subscriber notification selectively notifying only impacted selectors
"by-id key tracking": same concept, but uses proxies on a common "by-id" pattern we have. So instead of a selector subscribing to "all channels" via state.channels, it subscribes to an individual channel by id. This only works because we have common pattern where all reads/writes funnel through a known reducer
id-object-map-reducer: for these id-key to value reducers, we directly mutate the underlying object and use Object.create() to create a new object reference. This avoids cloning MASSIVE objects
render lanes: we created a series of context providers that provide stores with overridden subscribe methods to subtrees. these allow us to take really expensive trees out of normal subscriber notification, and let them be scheduled with things likescheduler.postTask
Proxies weren't fast enough for us. we had to defineProperty with each of the known keys of state and a get/set
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:
useSyncExternalStoreWithSelectormethod from theuse-sync-external-storepackage and converts it to TSuseSelectorto 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 specificuseTrackedSelectorhook), 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 gotsomething 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
useSyncExternalStoreWithSelectorto 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 existinghttps://github.com/reduxjs/react-redux-benchmarks using the new version, and it seems generally equivalent so far. At least it's notworse, 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 changinguseSelectorto 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.