简体   繁体   English

如何优化React + Redux中嵌套组件的道具的小更新?

[英]How to optimize small updates to props of nested component in React + Redux?

Example code: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js 示例代码: 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 观看现场演示: 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. 我有上面的组件,Repo和RepoList。 I want to update the tag of the first repo ( Line 14 ). 我想更新第一个仓库的标签( 第14行 )。 So I dispatched an UPDATE_TAG action. 所以我发送了一个UPDATE_TAG动作。 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. 在我实现shouldComponentUpdate之前,调度大约需要shouldComponentUpdate毫秒,这是预期的,因为我们浪费了大量时间来区分没有改变的<Repo/>

After added shouldComponentUpdate , dispatch takes about 30ms. 添加了shouldComponentUpdate ,dispatch大约需要30ms。 After production build React.js, the updates only cost at about 17ms. 在生产构建React.js之后,更新仅花费大约17ms。 This is much better, but timeline view in Chrome dev console still indicate jank frame (longer than than 16.6ms). 这要好得多,但Chrome开发者控制台中的时间线视图仍然表示jank帧(超过16.6ms)。

在此输入图像描述

Imagine if we have many updates like this, or <Repo/> is more complicated than current one, we won't be able to maintain 60fps. 想象一下,如果我们有这样的许多更新,或者<Repo/>比当前更复杂,我们将无法维持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? 我还可以使用Redux吗?

I got a solution by replacing every tags with an observable inside reducer. 通过用可观察的内部减速器替换每个tags ,我得到了一个解决方案。 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 . 然后我使用https://github.com/jayphelps/react-observable-subscribe在Repo组件中订阅它们的值。 This worked great. 这很有效。 Every dispatch only costs 5ms even with development build of React.js. 即使使用React.js的开发构建,每个调度仅花费5ms。 But I feel like this is an anti-pattern in Redux. 但我觉得这是Redux中的反模式。

Update 1 更新1

I followed the recommendation in Dan Abramov's answer and normalized my state and updated connect components 我按照Dan Abramov的回答中的建议, 将我的状态更新的连接组件 规范化

The new state shape is: 新的状态形状是:

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

I added console.time around ReactDOM.render to time the initial rendering . 我在ReactDOM.render周围添加了console.timeReactDOM.render 初始渲染时间。

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 ) (来源: https//github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js ,现场演示: 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

在此输入图像描述

I think connect on every <Repo/> has lots of overhead. 我认为每个<Repo/>上的连接都有很多开销。

Update 2 更新2

Based on Dan's updated answer, we have to return connect 's mapStateToProps arguments return an function instead. 根据Dan的更新答案,我们必须返回connectmapStateToProps参数,而不是返回一个函数。 You can check out Dan's answer. 你可以查看Dan的答案。 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 更新3

Just added react-virtualized example based on "connect at every with memorization" 刚刚添加了基于“每次记忆连接”的反应虚拟化示例

INITIAL: 31.878ms
DISPATCH: 4.549ms

I'm not sure where const App = connect((state) => state)(RepoList) comes from. 我不确定const App = connect((state) => state)(RepoList)来自const App = connect((state) => state)(RepoList)
The corresponding example in React Redux docs has a notice : React Redux文档中相应示例有一个通知

Don't do this! 不要这样做! It kills any performance optimizations because TodoApp will rerender after every action. 它会杀死任何性能优化,因为TodoApp会在每次操作后重新渲染。 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. 最好在视图层次结构中的几个组件上使用更细粒度的connect(),每个组件只监听状态的相关片段。

We don't suggest using this pattern. 我们不建议使用此模式。 Rather, each connect <Repo> specifically so it reads its own data in its mapStateToProps . 相反,每个都特别连接<Repo>因此它在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. 如果您的状态造型比较标准化 (现在它所有嵌套),可以单独repoIdsreposById ,然后只有你RepoList如果重新渲染repoIds变化。 This way changes to individual repos won't affect the list itself, and only the corresponding Repo will get re-rendered. 这种方式更改为单个repos不会影响列表本身,只会重新呈现相应的Repo 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. 真实世界 ”示例显示了如何编写处理规范化数据的Reducer。

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() : 请注意,为了真正从规范化树所提供的性能中获益,您需要执行与此拉取请求完全相同的操作,并将mapStateToProps()工厂传递给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. 这很重要的原因是因为我们知道ID永远不会改变。 Using ownProps comes with a performance penalty: the inner props have to be recalculate any time the outer props change. 使用ownProps会带来性能损失:内部道具必须在外部道具改变时重新计算。 However using initialOwnProps does not incur this penalty because it is only used once. 但是,使用initialOwnProps不会产生这种惩罚,因为它只使用一次。

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 . 请注意,我更改了ConnectedRepo connect()以使用具有initialOwnProps而非ownProps的工厂。 This lets React Redux skip all the prop re-evaluation. 这让React Redux跳过所有道具重新评估。

I also removed the unnecessary shouldComponentUpdate() on the <Repo> because React Redux takes care of implementing it in connect() . 我还删除了<Repo>上不必要的shouldComponentUpdate() ,因为React Redux负责在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. 如果它有副作用,它不是Redux减速器。 It may work, but I suggest to put code like this outside Redux to avoid confusion. 它可能有用,但我建议在Redux之外放置这样的代码以避免混淆。 Redux reducers must be pure functions, and they may not call onNext on subjects. Redux onNext必须是纯函数,并且它们可能不会在主题上调用onNext

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM