React 应用的性能优化思路

前端之家收集整理的这篇文章主要介绍了React 应用的性能优化思路前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

要点梗概

React 应用主要的性能问题在于多余的处理和组件的 DOM 比对。为了避免这些性能陷阱,你应该尽可能的在 shouldComponentUpdate 中返回 false

简而言之,归结于如下两点:

  1. 加速shouldComponentUpdate 的检查

  2. 简化shouldComponentUpdate 的检查

免责声明!

文章中的示例是用 React + Redux 写的。如果你用的是其它的数据流库,原理是相通的但是实现会不同。

文章中我没有使用 immutability (不可变)库,只是一些普通的 es6 和一点 es7。有些东西用不可变数据库要简单一点,但是我不准备在这里讨论这一部分内容

React 应用的主要性能问题是什么?

  1. 组件中那些不更新 DOM 的冗余操作

  2. DOM 比对那些无须更新的叶子节点

    • 虽则 DOM 比对很出色并加速了 React ,但计算成本是不容忽视的

React 默认的渲染行为是怎样的?

我们来看一下 React 是如何渲染组件的。

初始化渲染

在初始化渲染时,我们需要渲染整个应用

(绿色 = 已渲染节点)

每一个节点都被渲染 —— 这很赞!现在我们的应用呈现了我们的初始状态。

提出改变

我们想更新一部分数据。这些改变只和一个叶子节点相关

理想更新

我们只想渲染通向叶子节点的关键路径上的这几个节点

默认行为

如果你不告诉 React 别这样做,它便会如此

(橘黄色 = 浪费的渲染)

哦,不!我们所有的节点都被重新渲染了。

React 的每一个组件都有一个 shouldComponentUpdate(nextProps,nextState) 函数。它的职责是当组件需要更新时返回 true , 而组件不必更新时则返回 false 。返回 false 会导致组件的 render 函数不被调用。React 总是默认在 shouldComponentUpdate 中返回 true,即便你没有显示地定义一个 shouldComponentUpdate 函数

  1. // 默认行为
  2.  
  3. shouldComponentUpdate(nextProps,nextState) {
  4.  
  5. return true;
  6.  
  7. }

这就意味着在默认情况下,你每次更新你的顶层级的 props,整个应用的每一个组件都会渲染。这是一个主要的性能问题。

我们如何获得理想的更新?

尽可能的在 shouldComponentUpdate 中返回 false

简而言之:

  1. 加速shouldComponentUpdate 的检查

  2. 简化shouldComponentUpdate 的检查

加速 shouldComponentUpdate 检查

理想情况下我们不希望在 shouldComponentUpdate 中做深等检查,因为这非常昂贵,尤其是在大规模和拥有大的数据结构的时候。

  1. class Item extends React.component {
  2.  
  3. shouldComponentUpdate(nextProps) {
  4.  
  5. // 这很昂贵
  6.  
  7. return isDeepEqual(this.props,nextProps);
  8.  
  9. }
  10.  
  11. // ...
  12.  
  13. }

一个替代方法只要对象的值发生了变化,就改变对象的引用

  1. const newValue = {
  2.  
  3. ...oldValue
  4.  
  5. // 在这里做你想要的修改
  6.  
  7. };
  8.  
  9.  
  10.  
  11. // 快速检查 —— 只要检查引用
  12.  
  13. newValue === oldValue; // false
  14.  
  15.  
  16.  
  17. // 如果你愿意也可以用 Object.assign 语法
  18.  
  19. const newValue2 = Object.assign({},oldValue);
  20.  
  21.  
  22.  
  23. newValue2 === oldValue; // false

在 Redux reducer 中使用这个技巧:

  1. // 在这个 Redux reducer 中,我们将改变一个 item 的 description
  2.  
  3. export default (state,action) {
  4.  
  5.  
  6.  
  7. if(action.type === 'ITEM_DESCRIPTION_UPDATE') {
  8.  
  9.  
  10.  
  11. const { itemId,description } = action;
  12.  
  13.  
  14.  
  15. const items = state.items.map(item => {
  16.  
  17. // action 和这个 item 无关 —— 我们可以不作修改直接返回这个 item
  18.  
  19. if(item.id !== itemId) {
  20.  
  21. return item;
  22.  
  23. }
  24.  
  25.  
  26.  
  27. // 我们想改变这个 item
  28.  
  29. // 这会保留原本 item 的值,但
  30.  
  31. // 会返回一个更新过 description 的新对象
  32.  
  33. return {
  34.  
  35. ...item,description
  36.  
  37. };
  38.  
  39. });
  40.  
  41.  
  42.  
  43. return {
  44.  
  45. ...state,items
  46.  
  47. };
  48.  
  49. }
  50.  
  51.  
  52.  
  53. return state;
  54.  
  55. }

如果你采用这个方法,那你只需在 shouldComponentUpdate 函数中作引用检查

  1. // 超级快 —— 你所做的只是检查引用!
  2.  
  3. shouldComponentUpdate(nextProps) {
  4.  
  5. return isObjectEqual(this.props,nextProps);
  6.  
  7. }

isObjectEqual 的一个实现示例

  1. const isObjectEqual = (obj1,obj2) => {
  2.  
  3. if(!isObject(obj1) || !isObject(obj2)) {
  4.  
  5. return false;
  6.  
  7. }
  8.  
  9.  
  10.  
  11. // 引用是否相同
  12.  
  13. if(obj1 === obj2) {
  14.  
  15. return true;
  16.  
  17. }
  18.  
  19.  
  20.  
  21. // 它们包含的键名是否一致?
  22.  
  23. const item1Keys = Object.keys(obj1).sort();
  24.  
  25. const item2Keys = Object.keys(obj2).sort();
  26.  
  27.  
  28.  
  29. if(!isArrayEqual(item1Keys,item2Keys)) {
  30.  
  31. return false;
  32.  
  33. }
  34.  
  35.  
  36.  
  37. // 属性所对应的每一个对象是否具有相同的引用?
  38.  
  39. return item2Keys.every(key => {
  40.  
  41. const value = obj1[key];
  42.  
  43. const nextValue = obj2[key];
  44.  
  45.  
  46.  
  47. if(value === nextValue) {
  48.  
  49. return true;
  50.  
  51. }
  52.  
  53.  
  54.  
  55. // 数组例外,再检查一个层级的深度
  56.  
  57. return Array.isArray(value) &&
  58.  
  59. Array.isArray(nextValue) &&
  60.  
  61. isArrayEqual(value,nextValue);
  62.  
  63. });
  64.  
  65. };
  66.  
  67.  
  68.  
  69. const isArrayEqual = (array1 = [],array2 = []) => {
  70.  
  71. if(array1 === array2) {
  72.  
  73. return true;
  74.  
  75. }
  76.  
  77.  
  78.  
  79. // 检查一个层级深度
  80.  
  81. return array1.length === array2.length &&
  82.  
  83. array1.every((item,index) => item === array2[index]);
  84.  
  85. };

简化 shouldComponentUpdate 检查

先看一个复杂shouldComponentUpdate 示例

  1. // 关注分离的数据结构(标准化数据)
  2.  
  3. const state = {
  4.  
  5. items: [
  6.  
  7. {
  8.  
  9. id: 5,description: 'some really cool item'
  10.  
  11. }
  12.  
  13. ]
  14.  
  15.  
  16.  
  17. // 表示用户与系统交互的对象
  18.  
  19. interaction: {
  20.  
  21. selectedId: 5
  22.  
  23. }
  24.  
  25. };

如果这样组织你的数据,会使得在 shouldComponentUpdate 中进行检查变得困难

  1. import React,{ Component,PropTypes } from 'react'
  2.  
  3.  
  4.  
  5. class List extends Component {
  6.  
  7.  
  8.  
  9. propTypes = {
  10.  
  11. items: PropTypes.array.isrequired,iteraction: PropTypes.object.isrequired
  12.  
  13. }
  14.  
  15.  
  16.  
  17. shouldComponentUpdate (nextProps) {
  18.  
  19. // items 中的元素是否发生了改变?
  20.  
  21. if(!isArrayEqual(this.props.items,nextProps.items)) {
  22.  
  23. return true;
  24.  
  25. }
  26.  
  27.  
  28.  
  29. // 从这里开始事情会变的很恐怖
  30.  
  31.  
  32.  
  33. // 如果 interaction 没有变化,那可以返回 false (真棒!)
  34.  
  35. if(isObjectEqual(this.props.interaction,nextProps.interaction)) {
  36.  
  37. return false;
  38.  
  39. }
  40.  
  41.  
  42.  
  43. // 如果代码运行到这里,我们知道:
  44.  
  45. // 1. items 没有变化
  46.  
  47. // 2. interaction 变了
  48.  
  49. // 我们需要 interaction 的变化是否与我们相干
  50.  
  51.  
  52.  
  53. const wasItemSelected = this.props.items.any(item => {
  54.  
  55. return item.id === this.props.interaction.selectedId
  56.  
  57. })
  58.  
  59. const isItemSelected = nextProps.items.any(item => {
  60.  
  61. return item.id === nextProps.interaction.selectedId
  62.  
  63. })
  64.  
  65.  
  66.  
  67. // 如果发生了改变就返回 true
  68.  
  69. // 如果没有发生变化就返回 false
  70.  
  71. return wasItemSelected !== isItemSelected;
  72.  
  73. }
  74.  
  75.  
  76.  
  77. render() {
  78.  
  79. <div>
  80.  
  81. {this.props.items.map(item => {
  82.  
  83. const isSelected = this.props.interaction.selectedId === item.id;
  84.  
  85. return (<Item item={item} isSelected={isSelected} />);
  86.  
  87. })}
  88.  
  89. </div>
  90.  
  91. }
  92.  
  93. }

问题1:shouldComponentUpdate 体积庞大

你可以看出一个非常简单的数据对应的 shouldComponentUpdate 即庞大又复杂。这是因为它需要知道数据的结构以及它们之间的关联。shouldComponentUpdate 函数的复杂度和体积只随着你的数据结构增长。这很容易导致两点错误

  1. 在不应该返回 false 的时候返回 false(应用显示错误的状态)

  2. 在不应该返回 true 的时候返回 true(引发性能问题)

为什么要让事情变得这么复杂?你只想让这些检查变得简单一点,以至于你根本就不必考虑它们。

问题2:父子级之间强耦合

通常而言,应用都要推广松耦合(组件对其它的组件知道的越少越好)。父组件应该尽量避免知晓其子组件的工作原理。这就允许你改变子组件的行为而无须让父级知晓这些变化(假设 PropsTypes 保持不变)。它还允许子组件独立运转,而不必让父级紧密的控制其行为。

解决办法:压平你的数据

通过压平(合并)你的数据结构,你可以重新使用非常简单的引用检查来看是否有什么发生了变化。

  1. const state = {
  2.  
  3. items: [
  4.  
  5. {
  6.  
  7. id: 5,description: 'some really cool item',// interaction 现在存在于 item 的内部
  8.  
  9. interaction: {
  10.  
  11. isSelected: true
  12.  
  13. }
  14.  
  15. }
  16.  
  17. }
  18.  
  19. };

这样组织你的数据使得在 shouldComponentUpdate 中做检查变得简单

  1. import React,{Component,PropTypes} from 'react'
  2.  
  3.  
  4.  
  5. class List extends Component {
  6.  
  7.  
  8.  
  9. propTypes = {
  10.  
  11. items: PropTypes.array.isrequired
  12.  
  13. }
  14.  
  15.  
  16.  
  17. shouldComponentUpdate(nextProps) {
  18.  
  19. // so easy,麻麻再也不用担心我的更新检查了
  20.  
  21. return isObjectEqual(this.props,nextProps);
  22.  
  23. }
  24.  
  25.  
  26.  
  27. render() {
  28.  
  29. <div>
  30.  
  31. {this.props.items.map(item => {
  32.  
  33.  
  34.  
  35. return (
  36.  
  37. <Item item={item}
  38.  
  39. isSelected={item.interaction.isSelected} />)
  40.  
  41. })}
  42.  
  43. </div>
  44.  
  45. }
  46.  
  47. }

如果你想要更新 interaction 你就改变整个对象的引用

  1. // redux reducer
  2.  
  3. export default (state,action) => {
  4.  
  5.  
  6.  
  7. if(action.type === 'ITEM_SELECT') {
  8.  
  9.  
  10.  
  11. const { itemId } = action;
  12.  
  13.  
  14.  
  15. const items = state.items.map(item => {
  16.  
  17. if(item.id !== itemId) {
  18.  
  19. return item;
  20.  
  21. }
  22.  
  23.  
  24.  
  25. // 改变整个对象的引用
  26.  
  27. return {
  28.  
  29. ...item,interaction: {
  30.  
  31. isSelected: true
  32.  
  33. }
  34.  
  35. }
  36.  
  37. })
  38.  
  39.  
  40.  
  41. return {
  42.  
  43. ...state,items
  44.  
  45. };
  46.  
  47. }
  48.  
  49.  
  50.  
  51. return state;
  52.  
  53. };

误区:引用检查与动态 props

一个创建动态 props 的例子

  1. class Foo extends React.Component {
  2.  
  3. render() {
  4.  
  5. const {items} = this.props;
  6.  
  7.  
  8.  
  9. // 这个对象每次都有一个新的引用
  10.  
  11. const newData = { hello: 'world' };
  12.  
  13.  
  14.  
  15.  
  16.  
  17. return <Item name={name} data={newData} />
  18.  
  19. }
  20.  
  21. }
  22.  
  23.  
  24.  
  25. class Item extends React.Component {
  26.  
  27.  
  28.  
  29. // 即便前后两个对象的值相同,检查也总会返回true,因为 `data` 每次都会得到一个新的引用
  30.  
  31. shouldComponentUpdate(nextProps) {
  32.  
  33. return isObjectEqual(this.props,nextProps);
  34.  
  35. }
  36.  
  37. }

通常我们不会在组件中创建一个新的 props 把它传下来 。但是,这在循环中更为常见

  1. class List exntends React.Component {
  2.  
  3. render() {
  4.  
  5. const {items} = this.props;
  6.  
  7.  
  8.  
  9. <div>
  10.  
  11. {items.map((item,index) => {
  12.  
  13. // 这个对象每次都会获得一个新引用
  14.  
  15. const newData = {
  16.  
  17. hello: 'world',isFirst: index === 0
  18.  
  19. };
  20.  
  21.  
  22.  
  23.  
  24.  
  25. return <Item name={name} data={newData} />
  26.  
  27. })}
  28.  
  29. </div>
  30.  
  31. }
  32.  
  33. }

这在创建函数时很常见

  1. import myActionCreator from './my-action-creator';
  2.  
  3.  
  4.  
  5. class List extends React.Component {
  6.  
  7. render() {
  8.  
  9. const {items,dispatch} = this.props;
  10.  
  11.  
  12.  
  13. <div>
  14.  
  15. {items.map(item => {
  16.  
  17. // 这个函数的引用每次都会变
  18.  
  19. const callback = () => {
  20.  
  21. dispatch(myActionCreator(item));
  22.  
  23. }
  24.  
  25.  
  26.  
  27. return <Item name={name} onUpdate={callback} />
  28.  
  29. })}
  30.  
  31. </div>
  32.  
  33. }
  34.  
  35. }

解决问题的策略

  1. 避免在组件中创建动态的 props

改善你的数据模型,这样你就可以直接把 props 传下来

  1. 把动态 props 转化成满足全等(===)的类型传下来

eg:

  • boolean

  • number

  • string

  1. const bool1 = true;
  2.  
  3. const bool2 = true;
  4.  
  5.  
  6.  
  7. bool1 === bool2; // true
  8.  
  9.  
  10.  
  11. const string1 = 'hello';
  12.  
  13. const string2 = 'hello';
  14.  
  15.  
  16.  
  17. string1 === string2; // true

如果你实在需要传递动态对象,那就把它当作字符串传下来,再在子级进行解构

  1. render() {
  2.  
  3. const {items} = this.props;
  4.  
  5.  
  6.  
  7. <div>
  8.  
  9. {items.map(item => {
  10.  
  11. // 每次获得新引用
  12.  
  13. const bad = {
  14.  
  15. id: item.id,type: item.type
  16.  
  17. };
  18.  
  19.  
  20.  
  21. // 相同的值可以满足严格的全等 '==='
  22.  
  23. const good = `${item.id}::${item.type}`;
  24.  
  25.  
  26.  
  27. return <Item identifier={good} />
  28.  
  29. })}
  30.  
  31. </div>
  32.  
  33. }

特殊情况:函数

  1. 如果可以的话,尽量避免传递函数。相反,让子组件自由的 dispatch 动作。这还有个附加的好处就是把业务逻辑移出组件。

  2. shouldComponetUpdate 中忽略函数检查。这样不是很理想,因我们不知道函数的值是否变化了。

  3. 创建一个 data -> function 的不可变绑定。你可以在 componentWillReceiveProps 函数中把它们存到 state 中去。这样就不会在每一次 render 时拿到新的引用。这个方法极度笨重,因为你须要维护和更新一个函数列表。

  4. 创建一个拥有正确 this 绑定的中间组件。这也不够理想,因为你在层级中引入了一个冗余层。

  5. 任何其它你能够想到的、能够避免每次 render 调用时创建一个新函数方法

方案4 的示例

  1. // 引入另外一层 'ListItem'
  2.  
  3. <List>
  4.  
  5. <ListItem> // 你可以在这里创建正确的 this 绑定
  6.  
  7. <Item />
  8.  
  9. </ListItem>
  10.  
  11. </List>
  12.  
  13.  
  14.  
  15. class ListItem extends React.Component {
  16.  
  17.  
  18.  
  19. // 这样总能得到正确的 this 绑定,因为它绑定在了实例上
  20.  
  21. // 感谢 es7!
  22.  
  23. const callback = () => {
  24.  
  25. dispatch(doSomething());
  26.  
  27. }
  28.  
  29.  
  30.  
  31. render() {
  32.  
  33. return <Item callback={this.callback} item={this.props.item} />
  34.  
  35. }
  36.  
  37. }

工具

以上列出来的所有规则和技巧都是通过使用性能测量工具发现的。使用工具可以帮助你发现你的应用的具体性能问题所在。

console.time

这一个相当简单:

  1. 开始一个计时器

  2. 做点什么

  3. 停止计时器

一个比较好的做法是使用 Redux 中间件:

  1. export default store => next => action => {
  2.  
  3. console.time(action.type)
  4.  
  5.  
  6.  
  7. // `next` 是一个函数,它接收 'action' 并把它发送到 ‘reducers' 进行处理
  8.  
  9. // 这会导致你应有的一次重渲
  10.  
  11. const result = next(action);
  12.  
  13.  
  14.  
  15. // 渲染用了多久?
  16.  
  17. console.timeEnd(action.type);
  18.  
  19.  
  20.  
  21. return result;
  22.  
  23. };

用这个方法可以记录你应用的每一个 action 和它引起的渲染所花费的时间。你可以快速知道哪些 action 渲染时间最长,这样当你解决性能问题时就可以从那里着手。拿到时间值还能帮助你判断你所做的性能优化是否奏效了。

React.perf

这个工具的思路和 console.time 是一致的,只不过用的是 React 的性能工具:

  1. Perf.start()

  2. do stuff

  3. Perf.stop()

Redux 中间件示例:

  1. import Perf from 'react-addons-perf';
  2.  
  3.  
  4.  
  5. export default store => next => action => {
  6.  
  7. const key = `performance:${action.type}`;
  8.  
  9. Perf.start();
  10.  
  11.  
  12.  
  13. // 拿到新的 state 重渲应用
  14.  
  15. const result = next(action);
  16.  
  17. Perf.stop();
  18.  
  19.  
  20.  
  21. console.group(key);
  22.  
  23. console.info('wasted');
  24.  
  25. Perf.printWasted();
  26.  
  27. // 你可以在这里打印任何你感兴趣的 Perf 测量值
  28.  
  29.  
  30.  
  31. console.groupEnd(key);
  32.  
  33. return result;
  34.  
  35. };

console.time 方法类似,它能让你看到你每一个 action 的性能指标。更多关于 React 性能 addon 的信息请点击这里

浏览器工具

cpu 分析器火焰图表在寻找你的应用程序的性能问题时也能发挥作用。

在做性能分析时,火焰图表会展示出每一毫秒你的代码的 Javascript 堆栈的状态。在记录的时候,你就可以确切地知道任意时间点执行的是哪一个函数,它执行了多久,又是谁调用了它。—— Mozilla

Firefox: 点击查看

Chrome: 点击查看

感谢阅读,祝你顺利构建出高性能的 React 应用!

猜你在找的React相关文章