-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Optimize createListenerCollection #1523
Optimize createListenerCollection #1523
Conversation
}, | ||
|
||
notify() { | ||
const listeners = (current = next) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed current/next based on this comment #1450 (comment)
Deploy preview for react-redux-docs ready! Built with commit 47b5fb9 |
I would like to see the actual benchmarks you're using to compare these results first, and get a better understanding of the behavior characteristics may be different between the current implementation and this PR. |
This is the benchmark code: const store = createStore(
(state = 0, action) => (action.type === "INC" ? state + 1 : state)
);
const increment = () => ({ type: "INC" });
const App = connect(null, { increment })(
class extends React.Component {
state = { numChildren: 0 };
toggleChildren = () => {
this.setState(state => ({ numChildren: state.numChildren ? 0 : 50000 }));
};
render() {
return (
<div>
<button onClick={this.toggleChildren}>Toggle Children</button>
{[...Array(this.state.numChildren).keys()].map(i => (
<Child key={i} />
))}
</div>
);
}
}
);
const Child = connect(state => ({ state }))(
class extends React.Component {
render() {
return <div className="child">{this.props.state}</div>;
}
}
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
); What I'm profiling is the unmounting. This is obviously specifically designed to expose the quadratic complexity of the unsubscribe function. |
Can you clarify what specifically about the current implementation is "quadratic"? |
Okay, yeah, that definitely looks like an issue. |
This statement |
LGTM. It may be a tiny bit less memory efficient (there is a closure around |
Yeah, I vote we put this in, merge #1506 , and put out 7.2.0. |
I'll publish shortly. |
The Subscription impl originally used an array of listeners just like the actual Redux store, but in #1523 we changed it to be a linked list to fix perf issues. The use of `indexOf+splice` caused a quadratic cost to unmounting many components. In v8, I originally dropped use of `Subscription` to potentially save on bundle size. However, `Provider` still uses `Subscription`, so that will always be in the bundle. For this issue, a user pointed out that directly subscribing to the store brought back the original quadratic unsubscription behavior, which I've confirmed locally. Swapping `store.subscribe` for `subscription.addNestedSub` fixes that issue. I've added a unit test that mounts and unmounts a massive tree and measures the elapsed time. There's a distinct difference between the "correct" and quadratic behavior. Weirdly, there's also a big diff in "correct" time between React 18, and 17 + the "compat" entry point, and I have no idea why. It's not as bad as the quadratic time, but it's still pretty expensive. I've written the test to set an acceptable max unmount time based on which React version we're running against, with some buffer added.
Follow-up to #1454. Using a linked list to store listeners for fast removal. For comparison unmounting 50k connected children under a single parent.
Before:
Afterwards: