44
votes

Example code: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js

View live demo: http://d6u.github.io/example-redux-update-nested-props/one-connect.html

How to optimize small updates to props of nested component?

I have above components, Repo and RepoList. I want to update the tag of the first repo (Line 14). So I dispatched an UPDATE_TAG action. Before I implemented shouldComponentUpdate, the dispatch takes about 200ms, which is expected since we are wasting lots of time diffing <Repo/>s that haven't changed.

After added shouldComponentUpdate, dispatch takes about 30ms. After production build React.js, the updates only cost at about 17ms. This is much better, but timeline view in Chrome dev console still indicate jank frame (longer than than 16.6ms).

enter image description here

Imagine if we have many updates like this, or <Repo/> is more complicated than current one, we won't be able to maintain 60fps.

My question is, for such small updates to a nested component's props, is there a more efficient and canonical way to update the content? Can I still use Redux?

I got a solution by replacing every tags with an observable inside reducer. Something like

// inside reducer when handling UPDATE_TAG action
// repos[0].tags of state is already replaced with a Rx.BehaviorSubject
get('repos[0].tags', state).onNext([{
  id: 213,
  text: 'Node.js'
}]);

Then I subscribe to their values inside Repo component using https://github.com/jayphelps/react-observable-subscribe. This worked great. Every dispatch only costs 5ms even with development build of React.js. But I feel like this is an anti-pattern in Redux.

Update 1

I followed the recommendation in Dan Abramov's answer and normalized my state and updated connect components

The new state shape is:

{
    repoIds: ['1', '2', '3', ...],
    reposById: {
        '1': {...},
        '2': {...}
    }
}

I added console.time around ReactDOM.render to time the initial rendering.

However, the performance is worse than before (both initial rendering and updating). (Source: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js, Live demo: http://d6u.github.io/example-redux-update-nested-props/repo-connect.html)

// With dev build
INITIAL: 520.208ms
DISPATCH: 40.782ms

// With prod build
INITIAL: 138.872ms
DISPATCH: 23.054ms

enter image description here

I think connect on every <Repo/> has lots of overhead.

Update 2

Based on Dan's updated answer, we have to return connect's mapStateToProps arguments return an function instead. You can check out Dan's answer. I also updated the demos.

Below, the performance is much better on my computer. And just for fun, I also added the side effect in reducer approach I talked (source, demo) (seriously don't use it, it's for experiment only).

// in prod build (not average, very small sample)

// one connect at root
INITIAL: 83.789ms
DISPATCH: 17.332ms

// connect at every <Repo/>
INITIAL: 126.557ms
DISPATCH: 22.573ms

// connect at every <Repo/> with memorization
INITIAL: 125.115ms
DISPATCH: 9.784ms

// observables + side effect in reducers (don't use!)
INITIAL: 163.923ms
DISPATCH: 4.383ms

Update 3

Just added react-virtualized example based on "connect at every with memorization"

INITIAL: 31.878ms
DISPATCH: 4.549ms
1
I amended my answer.Dan Abramov
Example links are dead :/Frilox

1 Answers

59
votes

I’m not sure where const App = connect((state) => state)(RepoList) comes from.
The corresponding example in React Redux docs has a notice:

Don’t do this! It kills any performance optimizations because TodoApp will rerender after every action. It’s better to have more granular connect() on several components in your view hierarchy that each only listen to a relevant slice of the state.

We don’t suggest using this pattern. Rather, each connect <Repo> specifically so it reads its own data in its mapStateToProps. The “tree-view” example shows how to do it.

If you make the state shape more normalized (right now it’s all nested), you can separate repoIds from reposById, and then only have your RepoList re-render if repoIds change. This way changes to individual repos won’t affect the list itself, and only the corresponding Repo will get re-rendered. This pull request might give you an idea of how that could work. The “real-world” example shows how you can write reducers that deal with normalized data.

Note that in order to really benefit from the performance offered by normalizing the tree you need to do exactly like this pull request does and pass a mapStateToProps() factory to connect():

const makeMapStateToProps = (initialState, initialOwnProps) => {
  const { id } = initialOwnProps
  const mapStateToProps = (state) => {
    const { todos } = state
    const todo = todos.byId[id]
    return {
      todo
    }
  }
  return mapStateToProps
}

export default connect(
  makeMapStateToProps
)(TodoItem)

The reason this is important is because we know IDs never change. Using ownProps comes with a performance penalty: the inner props have to be recalculate any time the outer props change. However using initialOwnProps does not incur this penalty because it is only used once.

A fast version of your example would look like this:

import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider, connect} from 'react-redux';
import set from 'lodash/fp/set';
import pipe from 'lodash/fp/pipe';
import groupBy from 'lodash/fp/groupBy';
import mapValues from 'lodash/fp/mapValues';

const UPDATE_TAG = 'UPDATE_TAG';

const reposById = pipe(
  groupBy('id'),
  mapValues(repos => repos[0])
)(require('json!../repos.json'));

const repoIds = Object.keys(reposById);

const store = createStore((state = {repoIds, reposById}, action) => {
  switch (action.type) {
  case UPDATE_TAG:
    return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);
  default:
    return state;
  }
});

const Repo  = ({repo}) => {
  const [authorName, repoName] = repo.full_name.split('/');
  return (
    <li className="repo-item">
      <div className="repo-full-name">
        <span className="repo-name">{repoName}</span>
        <span className="repo-author-name"> / {authorName}</span>
      </div>
      <ol className="repo-tags">
        {repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)}
      </ol>
      <div className="repo-desc">{repo.description}</div>
    </li>
  );
}

const ConnectedRepo = connect(
  (initialState, initialOwnProps) => (state) => ({
    repo: state.reposById[initialOwnProps.repoId]
  })
)(Repo);

const RepoList = ({repoIds}) => {
  return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>;
};

const App = connect(
  (state) => ({repoIds: state.repoIds})
)(RepoList);

console.time('INITIAL');
ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('app')
);
console.timeEnd('INITIAL');

setTimeout(() => {
  console.time('DISPATCH');
  store.dispatch({
    type: UPDATE_TAG
  });
  console.timeEnd('DISPATCH');
}, 1000);

Note that I changed connect() in ConnectedRepo to use a factory with initialOwnProps rather than ownProps. This lets React Redux skip all the prop re-evaluation.

I also removed the unnecessary shouldComponentUpdate() on the <Repo> because React Redux takes care of implementing it in connect().

This approach beats both previous approaches in my testing:

one-connect.js: 43.272ms
repo-connect.js before changes: 61.781ms
repo-connect.js after changes: 19.954ms

Finally, if you need to display such a ton of data, it can’t fit in the screen anyway. In this case a better solution is to use a virtualized table so you can render thousands of rows without the performance overhead of actually displaying them.


I got a solution by replacing every tags with an observable inside reducer.

If it has side effects, it’s not a Redux reducer. It may work, but I suggest to put code like this outside Redux to avoid confusion. Redux reducers must be pure functions, and they may not call onNext on subjects.