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
base: master
Are you sure you want to change the base?
Conversation
const ConnectedPixel = connect( | ||
(initialState, initialProps) => { | ||
const { i, j } = initialProps | ||
return state => { |
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.
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.
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.
Hi @gaearon, not really getting this change, you're still using i
and j
here, how is this not looking at ownProps
?
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.
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.
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.
@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?
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.
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.
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.
@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.
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.
Yep, that's the case.
const { i, j } = initialProps | ||
return state => { | ||
const active = state.get(i + ',' + j) || false | ||
return active ? ACTIVE_PROPS : INACTIVE_PROPS |
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.
Memoization. Could do the same with a library like reselect but this seemed simpler.
}, | ||
(initialState, initialProps) => (dispatch) => { | ||
const { i, j } = initialProps | ||
return { |
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.
Same thing: no use recomputing this when props change.
} | ||
}; | ||
}, | ||
(stateProps, dispatchProps, ownProps) => ({ |
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.
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__() |
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.
You don't want to slow down the app in production even if user has DevTools
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 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.
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.
Fair enough as long as we don't measure with them enabled. I don't think anybody optimized Redux DevTools for perf.
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.
Yes. I have DevTools turned off when I measure it. I only enabled the DevTools before publishing to GitHub pages.
} | ||
}; | ||
}, | ||
(stateProps, dispatchProps, ownProps) => ({ |
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.
Curious why the need for a mergeProps
usage here?
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.
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.
The default one has to loop over keys of each object when assign
ing, this one knows the shape exactly.
Very nice. Would love to hear @jimbolla 's thoughts on the perf difference between 4.4.6 and 5.0. |
@markerikson I'm not sure what I'm looking at here. |
@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 Basically I'm curious if v5 actually makes things better or worse overall in variations on this benchmark. |
Thanks @gaearon for joining in! Seems scripting takes about 20% of the run time, so it’s much better than V1 and V2. @markerikson The reason I use This causes 32,768 components to go through React’s update cycle and end up with |
I think v5 might be faster for general case but slower for a stress test like this. |
The pattern where |
return active ? ACTIVE_PROPS : INACTIVE_PROPS | ||
} | ||
}, | ||
(initialState, initialProps) => (dispatch) => { |
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'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
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.
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 ↓
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'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.
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.
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.
Coming across the topic more than a year later - are the findings still the status quo? An update would be great! :) |
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 . |
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).