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

Add a simple but reasonably fast Redux version #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

gaearon
Copy link

@gaearon gaearon commented Dec 9, 2016

It's less efficient than V3 but also way simpler.
Relies on memoization of props (which is something you want to do in intensive apps).

This gives me ~20ms for going through 32K nodes which IMO is good enough for most real apps.

I used react-redux@4.x because it is the latest stable version and the one I optimized. I haven't participated in the 5.x effort and can't say if it is optimal for this scenario (but it does seem 1.5-2x slower).

const ConnectedPixel = connect(
(initialState, initialProps) => {
const { i, j } = initialProps
return state => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know i and j never change so we use a fast path: return a selector that doesn't look at its prop. Selectors that look at ownProps are much slower.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @gaearon, not really getting this change, you're still using i and j here, how is this not looking at ownProps?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it pulls i and j out of ownProps from the initial call, but then returns a function that only uses a single (state) argument. That way, connect will only run the returned function when the store state has actually changed, and not when the component's props have changed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markerikson Surely if props have changed then the whole mapStateToProps would be re-run returning a new instance of that function?

Is the fact that the function returns always the same ACTIVE_PROPS or INACTIVE_PROPS of any significance in all of that?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's the point of the "factory function" syntax.

If the first call to mapState returns a function instead of an object, then connect will use the returned function instead of the original function.

In either case, if the final mapState function has a signature of (state, ownProps), it will be called whenever either the store state or the wrapper's props have changed. If it has a signature of (state), it will only be called when the store state has changed.

So, in this example, the first mapState function has a (state, ownProps) signature, but the returned function only has a (state) signature, so it will be called less often.

As far as actually re-rendering, connect just checks to see if the object returned from mapState is shallow-equal to the previous result. Defining these results as constant references doesn't really matter, other than avoiding an object allocation each time.

Please see the React-Redux docs on using mapState functions for more details, as well as my blog post Idiomatic Redux: The Implementation and History of React-Redux.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markerikson Many thanks for quick response. I was confused thinking that the optimisation is done by returning ACTIVE_PROPS and INACTIVE_PROPS instead of the object, whereas the real optimisation is turned on by using a mapStateToProps function taking one argument instead of two.

I understand the same optimization would apply if mapStateToProps returned an arrow function taking one argument instead of two, as was the case originally?

You are right, it's all written in the documentation but I read it so many times yet failed to understand how the optimization works. I guess it's because it's the first time I am seeing some functionality/optimization enabled by the fact that a callback takes one instead of two arguments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's the case.

const { i, j } = initialProps
return state => {
const active = state.get(i + ',' + j) || false
return active ? ACTIVE_PROPS : INACTIVE_PROPS
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoization. Could do the same with a library like reselect but this seemed simpler.

},
(initialState, initialProps) => (dispatch) => {
const { i, j } = initialProps
return {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing: no use recomputing this when props change.

}
};
},
(stateProps, dispatchProps, ownProps) => ({
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't essential but since we know all the keys it might be more efficient to hand-roll it.

return originalCreateStore(reducer, extension)
let enhancer
if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) {
enhancer = window.__REDUX_DEVTOOLS_EXTENSION__()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to slow down the app in production even if user has DevTools

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree for real apps.

I kept it here so that the readers can try out the DevTools right on gh-pages without having to compile and run the app themselves. I also kept it in MobX version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough as long as we don't measure with them enabled. I don't think anybody optimized Redux DevTools for perf.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I have DevTools turned off when I measure it. I only enabled the DevTools before publishing to GitHub pages.

}
};
},
(stateProps, dispatchProps, ownProps) => ({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why the need for a mergeProps usage here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default one has to loop over keys of each object when assigning, this one knows the shape exactly.

@markerikson
Copy link

Very nice. Would love to hear @jimbolla 's thoughts on the perf difference between 4.4.6 and 5.0.

@jimbolla
Copy link

jimbolla commented Dec 9, 2016

@markerikson I'm not sure what I'm looking at here.

@markerikson
Copy link

@jimbolla : per Dan's comments at https://www.reddit.com/r/reactjs/comments/5hf4d4/an_artificial_example_where_mobx_really_shines/ and https://twitter.com/dan_abramov/status/807320728706150400 , the original version of this benchmark apparently was already using react-redux@next. Dan's new variation of this benchmark switched back to 4.4.6, and that plus the use of mapState factories made things faster.

Basically I'm curious if v5 actually makes things better or worse overall in variations on this benchmark.

@dtinth
Copy link
Owner

dtinth commented Dec 10, 2016

Thanks @gaearon for joining in!

Seems scripting takes about 20% of the run time, so it’s much better than V1 and V2.

screen shot 2016-12-10 at 10 14 41


@markerikson The reason I use react-redux@next is because of this line in master: the !this.doStatePropsDependOnOwnProps which says that if the computed state depended on own props, don’t early return.

This causes 32,768 components to go through React’s update cycle and end up with shouldComponentUpdate. This takes up a significant amount of time. By memoizing (using the initialProps instead of ownProps), this enables early return which is where perf gain came from. Somehow, it is slower in v5.

@gaearon
Copy link
Author

gaearon commented Dec 10, 2016

I think v5 might be faster for general case but slower for a stress test like this.
Not sure.

@jimbolla
Copy link

The pattern where mapStateToProps = (initialState, intialProps) => (justState) => results is the known case where v4 is faster than v5. v4's alogrith works really well for that usage. v5 benefits more from the mapStateToProps = (state, props) => results usage.

return active ? ACTIVE_PROPS : INACTIVE_PROPS
}
},
(initialState, initialProps) => (dispatch) => {
Copy link

@WiNloSt WiNloSt Dec 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious. Should the first parameter be dispatch? I see even though the mapDispatch is a factory, in the Redux source code it would still be passed dispatch in the first argument. If that's the case can we do this instead?

(dispatch, initialProps) => () => {
  const { i, j } = initialProps
  return {
    onToggle() {
      dispatch({ type: 'TOGGLE', i, j })
    }
  };
}

I just want to make sure I didn't miss anything :D

Copy link
Owner

@dtinth dtinth Dec 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. To benefit from memoization the most, should the function be generated outside the returned function?

(dispatch, initialProps) => {
  const { i, j } = initialProps
  const props = {
    onToggle() {
      dispatch({ type: 'TOGGLE', i, j })
    }
  }
  return () => props
}

EDIT: See this comment ↓

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that the () => props is a case of the "factory function" syntax, so that props and onToggle are only generated on the first call. Normally mapDispatch is only called when the component is created, unless you provide that second argument of ownProps, in which case connect will call it every time the incoming props change.

Since the returned function doesn't have 2 arguments defined, connect will detect that it doesn't need to be called repeatedly, as far as I know.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that’s right. Thanks for catching this! 😄

We’re just using the factory function just so that doDispatchPropsDependOnOwnProps is false and can let react-redux do its optimization.

@loopmode
Copy link

Coming across the topic more than a year later - are the findings still the status quo? An update would be great! :)

@markerikson
Copy link

I don't think anything has meaningfully changed.... yet. You may want to look at the React-Redux roadmap I put up for discussion in reduxjs/react-redux#950 .

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.

None yet

8 participants