要点梗概

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 函数。

// 默认行为shouldComponentUpdate(nextProps, nextState) {return true;}

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

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

尽可能的在 shouldComponentUpdate 中返回 false

简而言之:

  1. 加速shouldComponentUpdate 的检查

  2. 简化shouldComponentUpdate 的检查

加速 shouldComponentUpdate 检查

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

class Item extends React.component {shouldComponentUpdate(nextProps) {// 这很昂贵return isDeepEqual(this.props, nextProps);}// ...}

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

const newValue = {...oldValue// 在这里做你想要的修改};// 快速检查 —— 只要检查引用newValue === oldValue; // false// 如果你愿意也可以用 Object.assign 语法const newValue2 = Object.assign({}, oldValue);newValue2 === oldValue; // false

在 Redux reducer 中使用这个技巧:

// 在这个 Redux reducer 中,我们将改变一个 item 的 descriptionexport default (state, action) {if(action.type === 'ITEM_DESCRIPTION_UPDATE') {const { itemId, description } = action;const items = state.items.map(item => {// action 和这个 item 无关 —— 我们可以不作修改直接返回这个 itemif(item.id !== itemId) {return item;}// 我们想改变这个 item// 这会保留原本 item 的值,但// 会返回一个更新过 description 的新对象return {...item,description};});return {...state,items};}return state;}

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

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

isObjectEqual 的一个实现示例

const isObjectEqual = (obj1, obj2) => {if(!isObject(obj1) || !isObject(obj2)) {return false;}// 引用是否相同if(obj1 === obj2) {return true;}// 它们包含的键名是否一致?const item1Keys = Object.keys(obj1).sort();const item2Keys = Object.keys(obj2).sort();if(!isArrayEqual(item1Keys, item2Keys)) {return false;}// 属性所对应的每一个对象是否具有相同的引用?return item2Keys.every(key => {const value = obj1[key];const nextValue = obj2[key];if(value === nextValue) {return true;}// 数组例外,再检查一个层级的深度return Array.isArray(value) && Array.isArray(nextValue) && isArrayEqual(value, nextValue);});};const isArrayEqual = (array1 = [], array2 = []) => {if(array1 === array2) {return true;}// 检查一个层级深度return array1.length === array2.length &&array1.every((item, index) => item === array2[index]);};

简化 shouldComponentUpdate 检查

先看一个复杂shouldComponentUpdate 示例

// 关注分离的数据结构(标准化数据)const state = {items: [{id: 5,description: 'some really cool item'}]// 表示用户与系统交互的对象interaction: {selectedId: 5}};

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

import React, { Component, PropTypes } from 'react'class List extends Component {propTypes = {items: PropTypes.array.isRequired,iteraction: PropTypes.object.isRequired}shouldComponentUpdate (nextProps) {// items 中的元素是否发生了改变?if(!isArrayEqual(this.props.items, nextProps.items)) {return true;}// 从这里开始事情会变的很恐怖// 如果 interaction 没有变化,那可以返回 false (真棒!)if(isObjectEqual(this.props.interaction, nextProps.interaction)) {return false;}// 如果代码运行到这里,我们知道://    1. items 没有变化//    2. interaction 变了// 我们需要 interaction 的变化是否与我们相干const wasItemSelected = this.props.items.any(item => {return item.id === this.props.interaction.selectedId})const isItemSelected = nextProps.items.any(item => {return item.id === nextProps.interaction.selectedId})// 如果发生了改变就返回 true// 如果没有发生变化就返回 falsereturn wasItemSelected !== isItemSelected;}render() {<div>{this.props.items.map(item => {const isSelected = this.props.interaction.selectedId === item.id;return (<Item item={item} isSelected={isSelected} />);})}</div>}}

问题1:shouldComponentUpdate 体积庞大

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

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

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

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

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

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

解决办法:压平你的数据

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

const state = {items: [{id: 5,description: 'some really cool item',// interaction 现在存在于 item 的内部interaction: {isSelected: true}}}};

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

import React, {Component, PropTypes} from 'react'class List extends Component {propTypes = {items: PropTypes.array.isRequired}shouldComponentUpdate(nextProps) {// so easy,麻麻再也不用担心我的更新检查了return isObjectEqual(this.props, nextProps);}render() {<div>{this.props.items.map(item => {return (<Item item={item}isSelected={item.interaction.isSelected} />)})}</div>}}

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

// redux reducerexport default (state, action) => {if(action.type === 'ITEM_SELECT') {const { itemId } = action;const items = state.items.map(item => {if(item.id !== itemId) {return item;}// 改变整个对象的引用return {...item,interaction: {isSelected: true}}})return {...state,items};}return state;};

误区:引用检查与动态 props

一个创建动态 props 的例子

class Foo extends React.Component {render() {const {items} = this.props;// 这个对象每次都有一个新的引用const newData = { hello: 'world' };return <Item name={name} data={newData} />}}class Item extends React.Component {// 即便前后两个对象的值相同,检查也总会返回true,因为 `data` 每次都会得到一个新的引用shouldComponentUpdate(nextProps) {return isObjectEqual(this.props, nextProps);}}

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

class List exntends React.Component {render() {const {items} = this.props;<div>{items.map((item, index) => {// 这个对象每次都会获得一个新引用const newData = {hello: 'world',isFirst: index === 0};return <Item name={name} data={newData} />})}</div>}}

这在创建函数时很常见

import myActionCreator from './my-action-creator';class List extends React.Component {render() {const {items, dispatch} = this.props;<div>{items.map(item => {// 这个函数的引用每次都会变const callback = () => {dispatch(myActionCreator(item));}return <Item name={name} onUpdate={callback} />})}</div>}}

解决问题的策略

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

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

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

eg:

  • boolean

  • number

  • string

const bool1 = true;const bool2 = true;bool1 === bool2; // trueconst string1 = 'hello';const string2 = 'hello';string1 === string2; // true

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

render() {const {items} = this.props;<div>{items.map(item => {// 每次获得新引用const bad = {id: item.id,type: item.type};// 相同的值可以满足严格的全等 '==='const good = `${item.id}::${item.type}`;return <Item identifier={good} />})}</div>}

特殊情况:函数

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

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

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

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

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

方案4 的示例

// 引入另外一层 'ListItem'<List><ListItem> // 你可以在这里创建正确的 this 绑定<Item /></ListItem></List>class ListItem extends React.Component {// 这样总能得到正确的 this 绑定,因为它绑定在了实例上// 感谢 es7!const callback = () => {dispatch(doSomething());}render() {return <Item callback={this.callback} item={this.props.item} />}}

工具

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

console.time

这一个相当简单:

  1. 开始一个计时器

  2. 做点什么

  3. 停止计时器

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

export default store => next => action => {console.time(action.type)// `next` 是一个函数,它接收 'action' 并把它发送到 ‘reducers' 进行处理// 这会导致你应有的一次重渲const result = next(action);// 渲染用了多久?console.timeEnd(action.type);return result;};

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

React.perf

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

  1. Perf.start()

  2. do stuff

  3. Perf.stop()

Redux 中间件示例:

import Perf from 'react-addons-perf';export default store => next => action => {const key = `performance:${action.type}`;Perf.start();// 拿到新的 state 重渲应用const result = next(action);Perf.stop();console.group(key);console.info('wasted');Perf.printWasted();// 你可以在这里打印任何你感兴趣的 Perf 测量值console.groupEnd(key);return result;};

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

浏览器工具

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

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

Firefox: 点击查看

Chrome: 点击查看

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

React 应用的性能优化思路相关推荐

  1. 关于react hooks的性能优化

    目的 只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,比如 key 的使用.另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks 还是蛮难的. 面向读者 有过 ...

  2. React有哪些性能优化的手段?

    React有哪些性能优化的手段? 1.使用纯组件: 2.使用 React.memo 进行组件记忆(React.memo 是一个高阶组件),对 于相同的输入,不重复执行: 3.如果是类组件,使用 sho ...

  3. cpu 性能优化思路

    cpu 性能优化思路 文章目录 cpu 性能优化思路 前言 一.性能优化方法论 cpu性能优化工具 性能优化是否有效 多个性能问题同时存在,要怎么选择? 有多种优化方法时,要如何选择? 二.CPU 优 ...

  4. RPC框架性能优化思路和具体实现

    目录 RPC框架性能优化思路 动态代理对比 序列化框架对比 线程模型 路由层性能 RPC是什么? 应用场景 RPC 优点 写RPC框架需要具备哪些知识? RPC原理(摘自:什么情况下使用 RPC ? ...

  5. Web前端性能优化思路

    本文旨在整理常见Web前端性能优化的思路,可供前端开发参考.因为力求精简,限于篇幅,所以并未详述具体实施方案. 基于现代Web前端框架的应用,其原理是通过浏览器向服务器发送网络请求,获取必要的inde ...

  6. JAVA性能优化思路探究

    1.背景介绍 一个系统的上线除了常规的功能性测试外,还需要经过严格的性能测试,满足预期的性能指标(常见的有响应时间,tps等),才允许上生产环境.广义的性能测试一般还包含负载测试(用于测试系统的容量: ...

  7. 系统架构性能优化思路

    -     前言    - 今天谈下业务系统性能问题分析诊断和性能优化方面的内容.这篇文章重点还是谈已经上线的业务系统后续出现性能问题后的问题诊断和优化重点. 系统性能问题分析流程 我们首先来分析下如 ...

  8. 系统性能指标、压测、性能优化思路

    压测 压力测试考察的是当前软硬件环境下系统所能承受的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统在线上的处理能力和稳定性维持在一定的范围内,做到心中有数. 使用压力测试,我们有希望找到很多种用其 ...

  9. 深入学习React函数组件性能优化三剑客useMemo、useCallback、memo

    Hook使用规则 只能在函数的最外层调用Hook,不能在循环.条件判断或子函数中调用. 只能在React函数组件或自定义Hook中调用Hook,不可在其他JavaScript函数中使用. useMem ...

最新文章

  1. [TensorRT] ERROR: Network must have at least one output
  2. Python骚操作!你还在用PS制作电子签名吗?
  3. MSSQL返回季度开始月和某月是第几季度
  4. css布局:块级元素的居中
  5. Linux之shell脚本(2)
  6. WEB安全基础-URL跳转漏洞
  7. 新来个专家吐槽我们:连qps都不懂,靠谱吗?
  8. WhatFont——Google Chrome字体识别扩展
  9. 解决中64位Win7系统上PLSQL无法连接ORACLE的方法(PLSQL无法识别ORACLE_HOME的配置)
  10. 分析arm linux启动打印信息
  11. 计算机网络数据链路层之使用点对点信道
  12. 【原创】使用blockUI制作自定义的漂亮的网页提示框(代替confirm和alert)
  13. 无代码编程的兴起:借助无代码平台,无需编写任何基础代码即可开发软件
  14. PDF编辑器中文版怎么插入PDF空白页面
  15. 概率论中两个独立连续随机变量X,Y,变量Z=X+Y的密度函数为X,Y的卷积与特征函数原理
  16. 手机号码测试用例java_“邮箱”“验证码”“手机号码”输入框测试用例
  17. java版本PID放大/eTerm放大软件介绍
  18. MVX-Net: PointFusion 在mmdetection3d中的实现
  19. 库存遮羞布被揭开,高通提前发布骁龙8G2,国产手机已无路可走
  20. 雷曼另类“死因”:巴菲特漏看求救短信

热门文章

  1. Android开发——跟随手指的小球实现
  2. 《价值50亿的10句话》读后感(学生作业分享)
  3. 重定向dup2的本质
  4. 一个商场营销经理的实习总结
  5. c++ socket启动网卡接口
  6. socket编程 及select poll epoll示例
  7. C/C++ 笔试、面试题目大汇总
  8. ACE之Proactor模式使用实例
  9. Flask实战2问答平台--导航条
  10. Caffe官方教程翻译(6):Learning LeNet