如何在 React + Redux 中优化嵌套组件 props 的小更新? [英] How to optimize small updates to props of nested component in React + Redux?

查看:20
本文介绍了如何在 React + Redux 中优化嵌套组件 props 的小更新?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

示例代码:

想象一下,如果我们有很多这样的更新,或者 比当前更复杂,我们将无法维持 60fps.

我的问题是,对于嵌套组件的 props 如此小的更新,是否有更有效和规范的方式来更新内容?我还能使用 Redux 吗?

我得到了一个解决方案,将每个 tags 替换为一个可观察的内部减速器.类似的东西

//在处理 UPDATE_TAG 动作时在减速器内部//repos[0].tags of state 已经替换为 Rx.BehaviorSubjectget('repos[0].tags', state).onNext([{编号:213,文本:'Node.js'}]);

然后我使用

我认为在每个 上连接都有很多开销.

更新 2

基于 Dan 的更新答案,我们必须返回 connectmapStateToProps 参数,而是返回一个函数.你可以看看丹的回答.我还更新了演示.

下面,我的电脑上的性能要好得多.为了好玩,我还在我谈到的减速器方法中添加了副作用(source, demo)(真的不要使用它,它仅供实验使用).

//在 prod build 中(不是一般的,非常小的样本)//根连接初始:83.789 毫秒调度:17.332ms//在每个 <Repo/> 连接初始:126.557ms调度:22.573ms//在每个 <Repo/> 连接背诵初始:125.115ms调度:9.784ms//observables + reducer 的副作用(不要使用!)初始:163.923ms调度:4.383ms

更新 3

刚刚添加了基于react-virtualized example关于每时每刻都记住"

初始:31.878ms调度:4.549ms

解决方案

我不确定 const App = connect((state) => state)(RepoList) 从何而来.
对应的React Redux 文档中的示例有一个通知:

<块引用>

不要这样做!它会扼杀任何性能优化,因为 TodoApp 会在每次操作后重新渲染.最好在视图层次结构中的几个组件上使用更细粒度的 connect(),每个组件都只聆听状态的相关片段.

我们不建议使用这种模式.相反,每个都专门连接 ,以便它在其 mapStateToProps 中读取自己的数据.tree-view"示例展示了如何做.

如果你让状态形状更加规范化(现在都是嵌套的),你可以分离repoIdscode> 来自 reposById,然后只有在 repoIds 更改时才重新渲染 RepoList.这样对单个 repos 的更改不会影响列表本身,只有相应的 Repo 会被重新渲染.这个拉取请求可能会让你了解它是如何工作的.真实世界"示例展示了如何编写处理规范化数据的 reducer.

请注意,为了真正从规范化树所提供的性能中受益,您需要像此拉取request 执行并将 mapStateToProps() 工厂传递给 connect():

const makeMapStateToProps = (initialState, initialOwnProps) =>{const { id } = initialOwnPropsconst mapStateToProps = (状态) =>{const { 待办事项 } = 状态const todo = todos.byId[id]返回 {去做}}返回 mapStateToProps}导出默认连接(makeMapStateToProps)(待办事项)

这很重要的原因是我们知道 ID 永远不会改变.使用 ownProps 会带来性能损失:每当外部 props 发生变化时,必须重新计算内部 props.然而,使用 initialOwnProps 不会招致这种惩罚,因为它只使用一次.

示例的快速版本如下所示:

从'react'导入React;从 'react-dom' 导入 ReactDOM;从redux"导入{createStore};从'react-redux'导入{Provider,connect};从 'lodash/fp/set' 导入集;从 'lodash/fp/pipe' 导入管道;从 'lodash/fp/groupBy' 导入 groupBy;从 'lodash/fp/mapValues' 导入 mapValues;const UPDATE_TAG = 'UPDATE_TAG';const reposById = 管道(groupBy('id'),mapValues(repos => repos[0]))(require('json!../repos.json'));const repoIds = Object.keys(reposById);const store = createStore((state = {repoIds, reposById}, action) => {开关(动作.类型){案例 UPDATE_TAG:return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);默认:返回状态;}});const Repo = ({repo}) =>{const [authorName, repoName] = repo.full_name.split('/');返回 (<li className="repo-item"><div className="repo-full-name"><span className="repo-name">{repoName}</span><span className="repo-author-name">/{作者姓名}</span>

<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>);}const ConnectedRepo = 连接((initialState, initialOwnProps) =>(状态) =>({回购:state.reposById[initialOwnProps.repoId]}))(回购);const RepoList = ({repoIds}) =>{return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>;};const App = 连接((状态) =>({repoIds: state.repoIds}))(RepoList);console.time('INITIAL');ReactDOM.render(<提供者商店={商店}><应用程序/></提供者>,document.getElementById('app'));console.timeEnd('INITIAL');setTimeout(() => {console.time('调度');store.dispatch({类型:UPDATE_TAG});console.timeEnd('调度');}, 1000);

请注意,我将 ConnectedRepo 中的 connect() 更改为使用带有 initialOwnProps 而不是 ownProps 的工厂.这让 React Redux 跳过所有 prop 重新评估.

我还删除了 <Repo> 上不必要的 shouldComponentUpdate() 因为 React Redux 负责在 connect() 中实现它.

这种方法在我的测试中胜过之前的两种方法:

one-connect.js:43.272ms更改前的 repo-connect.js:61.781 毫秒更改后的 repo-connect.js:19.954 毫秒

最后,如果您需要显示如此大量的数据,它无论如何都无法适应屏幕.在这种情况下,更好的解决方案是使用 虚拟化表,这样您就可以渲染数千行,而不会产生实际的性能开销显示它们.

<小时><块引用>

我得到了一个解决方案,用一个可观察的内部减速器替换每个标签.

如果它有副作用,那就不是 Redux reducer.它可能有效,但我建议将这样的代码放在 Redux 之外以避免混淆.Redux reducer 必须是纯函数,并且不能在主题上调用 onNext.

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).

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

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

解决方案

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.

这篇关于如何在 React + Redux 中优化嵌套组件 props 的小更新?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆