Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Rewrite connect() for better performance and extensibility#416

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
jimbolla wants to merge76 commits intoreduxjs:masterfromjimbolla:connect-rewrite
Closed

Rewrite connect() for better performance and extensibility#416

jimbolla wants to merge76 commits intoreduxjs:masterfromjimbolla:connect-rewrite

Conversation

@jimbolla
Copy link
Contributor

@jimbollajimbolla commentedJun 24, 2016
edited by timdorr
Loading

Update: Released as alpha!

See below. You can now install this asreact-redux@next:

npm install --save react-redux@next

Please test it out!

TL;DR

Rewroteconnect, same basic API plus advanced options/API, all tests pass, roughly 8x faster, more modular/extensible design

Overview

I rewroteconnect into modular pieces because I wanted to be able to extend with custom behavior in my own projects. Now connect is a facade aroundconnectAdvanced, by passing it a compatibleselectorFactory function.

I also was able to greatly improve performance by changing the store subscriptions to execute top-down to work with React's natural flow of updates; component instances lower in the tree always get updated after those above them, avoiding unnecessary re-renders.

Design/Architecture

I split the originalconnect into many functions+files to compartmentalize concepts for better readability and extensibility. The important pieces:

  • components/
    • connectAdvanced.js: the HOC that connects to the store and determines when to re-render
    • Provider.js: (hasn't changed)
  • selectors/
    • connect.js: composes the other functions into a fully-compatible API, by creating a selectorFactory and options object to pass toconnectAdvanced.
      that performs memoiztion and detects if the first run returns another function, indicating a factory
    • mapDispatchToProps.js: used to create a selector factory from themapDispatchToProps parameter, to be passed toselectorFactory.js asinitMapDispatchToProps. Detects whethermapDispatchToProps is missing, an object, or a function
    • mapStateToProps.js: used to create a selector factory from themapStateToProps parameter, to be passed toselectorFactory.js asinitMapStateToProps. Detects whethermapStateToProps is missing or a function
    • mergeProps.js: used to create a selector factory from themergeProps parameter, to be passed toselectorFactory.js asinitMergeProps. Detects whethermergeProps is missing or a function.
  • selectorFactory.js: givendispatch,pure,initMapStateToProps,initMapDispatchToProps, andinitMergeProps, creates aconnectAdvanced-compatible selector
  • wrapMapToProps.js: helper functions for wrapping values ofmapStateToProps andmapDispatchToProps in compatible selector factories

  • utils/
    • Subscription.js: encapsulates the hierachial subscription concept. used byconnectAdvanced.js to pass a parent's store Subscription to its children via context
    • verifyPlainObject.js: used to show a warning ifmapStateToProps,mapDispatchToProps, ormergeProps returns something other than a plain object

file graph

graph

digraph files {  "connectAdvanced.js" -> "React"  "connectAdvanced.js" -> "Subscription.js"  "connect.js" -> "connectAdvanced.js"  "connect.js" -> "mapDispatchToProps.js"  "connect.js" -> "mapStateToProps.js"  "connect.js" -> "mergeProps.js"  "connect.js" -> "selectorFactory.js"  "mapDispatchToProps.js" -> "wrapMapToProps.js"  "mapStateToProps.js" -> "wrapMapToProps.js"}

The modular structure of all the functions inconnect/ should allow greater reuse for anyone that wants to create their ownconnect variant. For example, one could create a variant that handles whenmapStateToProps is an object by using reselect's createStructuredSelector:

customConnect.js:

importconnectfrom'react-redux/lib/connect/connect'importdefaultMapStateToPropsFactoriesfrom'react-redux/lib/connect/mapStateToProps'functioncreateStatePropsSelectorFromObject(mapStateToProps){constinitSelector=()=>createStructuredSelector(mapStateToProps)initSelector.dependsOnProps=truereturninitSelector}functionwhenStateIsObject(mapStateToProps){return(mapStateToProps&&typeofmapStateToProps==='object')    ?createStatePropsSelectorFromObject(mapStateToProps)    :undefined}}exportdefaultfunctioncustomConnect(mapStateToProps,mapDispatchToProps,mergeProps,options){returnconnect(mapStateToProps,mapDispatchToProps,mergeProps,{    ...options,mapStateToPropsFactories:defaultMapStateFactories.concat(whenStateIsObject)})}

ExampleComponent.js

// ...exportdefaultcustomConnect({thing:thingSelector,thong:thongSelector})(ExampleComponent)

And for scenarios where connect's three-function API is too constrictive, one can directly call, or build a wrapper around,connectAdvanced where they have full control over turningstate +props +dispatch into a props object.

Performance

I'm using amodified version of react-redux-perf to performance test+profile the changes. It's configured to try to fire up to 200 actions per second (but becomes CPU bound), with 301 connected components. There are 2 scenarios being tested:

  • NB: a parent component with 300 child components, with no other React components between them.
  • WB: the same setup as NB but there's a "Blocker" React component between the parent and children that always returns false forshouldComponentUpdate.

I measured the milliseconds needed to render a frame using thestats.js used by react-redux-perf:

MS: avg (min-max)current NBrewrite NBcurrent WBrewrite WB
Chrome170 (159-223)20 (17-55)170 (167-231)17 (15-59)
Firefox370 (331-567)20 (16-51)430 (371-606)19 (15-60)
IE11270 (127-301)40 (36-128)300 (129-323)33 (30-124)
Edge240 (220-371)37 (32-102)260 (97-318)28 (24-100)

On the conservitive end, the rewrite is about 8x faster under these circumstances, with Firefox even doubling that improvement. Much of the perf gains are attributed to avoiding calls tosetState() after a store update unless a re-render is necessary.

In order to make that work with nested connected components, store subscriptions were changed from "sideways" to top-down; parent components always update before their child components. Connected components detected whether they are nested by looking for an object of typeSubscription in the Reactcontext with the keystoreSubscription. This allowsSubscription objects build into a composite pattern.

After that I've used Chrome and Firefox's profilers to watch for functions that could be optimized. At this point, the most expensive method isshallowEqual, accounting for 4% and 1.5% CPU in Chrome and Firefox, respectively.

connectAdvanced(selectorFactory, options) API

In addition to the changed related to performance, the other key change is an additional API forconnectAdvanced().connectAdvanced is now the base forconnect but is less opinionated about how to combinestate,props, anddispatch. It makes no assumptions about defaults or intermediate memoization of results, and leaves those concerns up to the caller. It does memoize the inbound and outbound props objects. A full signature forconnectAdvanced with itsselectorFactory would look like:

exportdefaultconnectAdvanced(// selectorFactory receives dispatch and lots of metadata as named parameters. Most won't be// useful to a direct caller, but could be useful to a library author wrapping connectAdvanced(dispatch,options)=>(nextState,nextProps)=>({someProp://...anotherProp://...}),// options with their defaults:{getDisplayName:name=>`ConnectAdvanced(${name})`,methodName:'connectAdvanced',renderCountProp:undefined,shouldHandleStateChange:true,storeKey:'store',withRef:false,// you can also add extra options that will be passed through to your selectorFactory:custom1:'hey',fizzbuzz:fizzbuzzFunc})(MyComponent)

A simple usage may look like:

exportdefaultconnectAdvanced(dispatch=>{constbound=bindActionCreators(actionCreators,dispatch)return(store,props)=>({thing:state.things[props.thingId],saveThing:bound.saveThing})})(MyComponent)

An example usingreselect to create a bound actionCreator with a prop partially bound:

exportdefaultconnectAdvanced(dispatch=>{returncreateStructuredSelector({thing:(state,props)=>state.things[props.thingId],saveThing:createSelector((_,props)=>props.thingId,(thingId)=>inputs=>dispatch(actionCreators.saveThing(thingId,inputs)))})})(MyComponent)

An example doing custom memoization with actionCreator with a prop partially bound:

exportdefaultconnectAdvanced(dispatch=>{returncreateStructuredSelector({letthingIdletthingletresultconstsaveThing=inputs=>dispatch(actionCreators.saveThing(thingId,inputs))return(nextState,nextProps)=>{const nextThingId=nextProps.thingIdconstnextThing=nextState.things[nextThingId]if(nextThingId!==thingId||nextThing!==thing){        thingId=nextThingIdthing=nextThingresult={ thing, saveThing}}returnresult}})})(MyComponent)

Note these are meant as examples and not necessarily "best practices."

Pros/cons

I understand there is great risk to accepting such drastic changes, that would have to be justified with significant benefits. I'll reiterate the two main benefits I believe these changes offer:

  1. Performance: There's potentially huge perf gains in situations where the number of connected components is high, stemming from conceptual changes to subscriptions so they go with the natural flow of events in React vs across them, as well as method profiling+optimizing using Chrome/FF.
  2. Extensibility/Maintainability: By splitting the responibilities of connect into many smaller functions, it should be easier both for react-redux contributors to work with the codebase and end users to extend its functionality though the additional APIs. If users can add their desired features in their own projects, that will reduce the number of feature requests to the core project.

Despite passing all the automated tests as well as week of manual testing, there is risk of impacting users dependent on implicit behavior, or that performance is worse in some unexpected circumstances. To minimize risk of impacting end users and downstream library authors, I think it would be wise to pull these changes into a "next" branch and first release an alpha package. This would give early adopters a chance to test it and provide feedback

Thanks

I'd like to thank the other github users who have so far offered feedback on these changes in#407, especially@markerikson who has gone above and beyond.

umbertoo, echenley, joeybaker, vyorkin, dashed, markerikson, foiseworth, emmadavieswilcox, insin, jayphelps, and 137 more reacted with thumbs up emojijacobp100, r3nya, meriadec, adi-pjackson, awendland, Kerumen, emmadavieswilcox, arusakov, traviscooper, rickhanlonii, and 45 more reacted with hooray emojiRestuta, Sly777, meriadec, grrowl, no23reason, awendland, emmadavieswilcox, konradk, arusakov, eisisig, and 43 more reacted with heart emoji
…ops pass. BREAKING CHANGE: requires setting an option parameter mapStateIsFactory/mapDispatchIsFactory to signal factory methods.
…hind-the-scenes behavior of new implementation.
…omponents always subscribe before child components, so that parents always update before children.
…ead of subscribing to the store directly they subscribe via their ancestor connected component. This ensures the ancestor gets to update before its children.
…st compare last rendered props to new selected props
@chrisvasz
Copy link

Love this! I just dropped the beta into an existing application that uses Redux to store form information. In my application, each field is connected to the store, similar toredux-form's approach after the v6 upgrade. I loaded a fairly complex form and noticed withPerf.printWasted() that each field was performing a "wasted" render each time any other field changed. This quickly adds up to hundreds of wasted renders on a medium-sized form. (Imagine that each character in an<input> causes a state change in the store. For an average typist, that is 3-4 characters per second. Multiply that by the number of form fields and you arrive at hundreds of wasted rendersper second on a form with just 25 fields, which is a small number for our use case.)

With the newreact-redux, against the exact same code, the hundreds of wasted renders have dropped to zero! Love it! Way to go@jimbolla!

jimbolla, markerikson, echenley, kainoa21, hueypeard, langpavel, elijahdorman, and MichaelDeBoey reacted with hooray emojitimdorr, langpavel, and MichaelDeBoey reacted with heart emoji

@Mike-Sinistralis
Copy link

Mike-Sinistralis commentedSep 30, 2016
edited
Loading

Dropped this into the same application I tested earlier. This test was performed on mock data, so the data is slightly different but the structure, relationship, etc of the data are all the same across both.

The test was a drag and drop implementation using lists and smart components. The difference with this change is MASSIVE in terms of render count. (This is on Chrome, which seems to be the browser best able to handle this scenario. Mobile fares much, much worse)

Before:
capture

After:
capture2

timdorr, davidpelayo, wedneyyuri, hpurmann, tadjohnston, MichaelDeBoey, robophil, and h2soheili reacted with thumbs up emojimarkerikson, slorber, chrisvasz, MichaelDeBoey, robophil, and h2soheili reacted with hooray emoji

@slorber
Copy link
Contributor

Hi,

This is good news, but can someone explain the performance improvements? I'm not sure to understand why it performs better than before as a drop-in replacement

zhengjunxin reacted with heart emoji

@markerikson
Copy link
Contributor

@jimbolla can explain further, but it's a combination of several factors. Primarily, there's a ton of custom memoization going on using selectors, which avoids the need to callsetState. v4, on the other hand, calledsetState and then did the real checking in the wrapper'srender function. It also ensures that subscriptions are handled top-down, fixing a loophole where children could get notified by the store before ancestors and causing unnecessary render checks. See the rest of this thread, as well as#407 for discussion and details.

@jimbolla
Copy link
ContributorAuthor

@slorber@markerikson The short short version... Forcing the order of components' store subscriptions to trigger top-down allows connect to avoid extra calls to setState, render, and mapStateToProps'n'friends.

@Mike-Sinistralis
Copy link

Doesn't that mean it also fixes the issue where can you end up getting components in in-determinant states because they render before their parent, which could result in a child trying to access non-existent data because they haven't been unmounted yet?

I've actually run into this problem, and tested to see if the issue still occurs with v5 and it does not, so another issue fixed?

@markerikson
Copy link
Contributor

Y'know, that's a good point. I've seen that in my own prototype app, where I frequently use connected parents rendering connected list items. I bet there's a good chance v5 resolves that.

@jimbolla
Copy link
ContributorAuthor

Yep. That was actually the motivation for making the change. The perf was a nice surprise.

langpavel, clbn, natevw, thomasingalls, tadjohnston, MichaelDeBoey, chentsulin, and imashu92 reacted with heart emoji

@slorber
Copy link
Contributor

slorber commentedOct 3, 2016
edited
Loading

That's nice! so finally it somehow breaks compatibility (not in a way that's likely to break someone's app) and the new behavior makes connect more performant, in addition to being more flexible.

However@jimbolla can you take a look at this usecase of a list of 100k connected elements?http://stackoverflow.com/a/34788570/82609
I've seen your comments pointing out that the new implementation will permit to solve this problem in an efficient way, but I'm not sure how it can be done. Any code to share?

@jimbolla
Copy link
ContributorAuthor

@slorber A few thoughts on that:

  • Can squeeze some extra perf by usingconnectAdvanced with a custom selector instead ofconnect. It would probably be worth it at 100k connected components.
  • Another option besides those you suggested would be grouping your components into pages of (let's say) 100, so instead of 100k connected components, you'd have 1000 connected pages containing 100 unconnected components. Not sure if this would be faster or not. But it's worth experimenting
  • In v5, connect/connectAdvanced take an optionshouldHandleStateChanges. (connect sets it based on whether mapStateToProps is provided, connectAdvanced defaults it to true) You could connect a component that reads from state, but doesn't subscribe to store changes. This means it would only trigger rerenders when it receives new props from above. This may be be faster or slower.

@jide
Copy link

jide commentedOct 5, 2016

Hi !

Since I updated a project to react-redux 5.0.0-beta.2, I see this error popping up, any clue ?
capture d ecran 2016-10-05 a 15 43 28
Which comes from :

      if (typeof ownProps[key] !== 'undefined') {        (0, _warning2.default)(false, 'Duplicate key ' + key + ' sent from both parent and state');        break;      }

@timdorr
Copy link
Member

@jide Looks like we're callingwarning() incorrectly in that commit. I'll fix and push out another beta.

@jide
Copy link

jide commentedOct 5, 2016

Ok thanx !

@timdorr
Copy link
Member

OK, pushed beta.3 to remove that check for now.

jide, slorber, and langpavel reacted with thumbs up emoji

clbn added a commit to clbn/freefeed-gamma that referenced this pull requestDec 4, 2016
On deleting comments, the app might want to re-render already removedPostComment before updating PostComments (which would prevent thatby removing that comment ID from the list in the first place).Actually, it will be fixed by migrating to react-redux@5, since thatversion will be forcing the order of components' store subscriptions totrigger top-down (seereduxjs/react-redux#416 (comment)plus next three comments). However, we cannot just sit and wait forstable react-redux@5, right?N.B. It's part of [components-rewiring], since the issue with racecondition was introduced by PostComment rewiring (connecting directlyto the store) in one of previous changes.
returnfalse
}
for(letkeyinb){
if(hasOwn.call(b,key))countB++
Copy link

@jcreadyjcreadyDec 14, 2016
edited
Loading

Choose a reason for hiding this comment

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

This could exit before completely iterating through all theb keys by checking ifcountA is less thancountB in each iteration:

for(letkeyinb){if(hasOwn.call(b,key)&&countA<++countB)returnfalse}returntrue

Copy link
Member

Choose a reason for hiding this comment

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

A PR to change that out would be appreciated!

jimbolla, robophil, and avinesh-impact reacted with thumbs up emoji
@natevw
Copy link

Looks like this has been released. Updating to 5.0.4 fixed some seemingly-bizarre bugs (caused by a previously-incorrect assumption that props were reliably passed from the top down), and haven't noticed any regressions. Thanks for making this happen!

markerikson, Restuta, robophil, and jakubkottnauer reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@timdorrtimdorrtimdorr left review comments

+1 more reviewer

@jcreadyjcreadyjcready left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

5.0

Development

Successfully merging this pull request may close these issues.

24 participants

@jimbolla@gnoff@gaearon@ellbee@markerikson@Mike-Sinistralis@timdorr@PCreations@stevewillard@max-mykhailenko@zavan@OshotOkill@slorber@irvinebroque@psulek@cpsubrian@chrisvasz@jide@natevw@jcready@davidkpiano@luqmaan@emmadavieswilcox@wtgtybhertgeghgtwtg

[8]ページ先頭

©2009-2025 Movatter.jp