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

Optimize createListenerCollection #1523

Merged

Conversation

wurstbonbon
Copy link
Contributor

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:
before

Afterwards:
afterwards

},

notify() {
const listeners = (current = next)
Copy link
Contributor Author

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)

@netlify
Copy link

netlify bot commented Feb 17, 2020

Deploy preview for react-redux-docs ready!

Built with commit 47b5fb9

https://deploy-preview-1523--react-redux-docs.netlify.com

@markerikson
Copy link
Contributor

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.

@wurstbonbon
Copy link
Contributor Author

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.

@markerikson
Copy link
Contributor

Can you clarify what specifically about the current implementation is "quadratic"?

@markerikson
Copy link
Contributor

Okay, yeah, that definitely looks like an issue.

@wurstbonbon
Copy link
Contributor Author

This statement next.splice(next.indexOf(listener), 1) for every listener that is remove we need look at all other listeners. And the splice should also scale with size of the array.

@wurstbonbon
Copy link
Contributor Author

Screenshot 2020-02-17 at 22 54 16

@timdorr
Copy link
Member

timdorr commented Feb 18, 2020

LGTM. It may be a tiny bit less memory efficient (there is a closure around listener that keeps the ref alive), but I think that's an acceptable tradeoff when this gets noticeable on something like a route change.

@markerikson
Copy link
Contributor

Yeah, I vote we put this in, merge #1506 , and put out 7.2.0.

@markerikson markerikson merged commit e649fb6 into reduxjs:master Feb 18, 2020
@markerikson
Copy link
Contributor

I'll publish shortly.

@markerikson
Copy link
Contributor

And https://github.com/reduxjs/react-redux/releases/tag/v7.2.0 is live.

@wurstbonbon wurstbonbon deleted the optimize-create-listener-collection branch August 8, 2021 18:12
markerikson added a commit that referenced this pull request Feb 5, 2022
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.
albertodev7 pushed a commit to albertodev7/react-redux that referenced this pull request Dec 8, 2022
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

Successfully merging this pull request may close these issues.

3 participants