浅谈react性能优化的方法


Posted in Javascript onSeptember 05, 2018

React性能优化思路

软件的性能优化思路就像生活中去看病,大致是这样的:

使用工具来分析性能瓶颈(找病根)

尝试使用优化技巧解决这些问题(服药)

使用工具测试性能是否确实有提升(疗效确认)

初识react只是为了尽快完成项目,后期进行代码审查时候发现有很多地方需要优化,因此做了个小结。

  • Code Splitting
  • shouldComponentUpdate避免重复渲染
  • 使用不可突变数据结构
  • 组件尽可能的进行拆分、解耦
  • 列表类组件优化
  • bind函数优化
  • 不要滥用props
  • ReactDOMServer进行服务端渲染组件

Code Splitting

Code Splitting 可以帮你“懒加载”代码,如果你没办法直接减少应用的体积,那么不妨尝试把应用从单个 bundle 拆分成单个 bundle + 多份动态代码的形式。

webpack提供三种代码分离方法,详情见webpack官网

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 SplitChunks 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

在此,主要了解一下第三种动态导入的方法。

1、例如可以把下面的import方式

import { add } from './math';
console.log(add(16, 26));

改写成动态 import 的形式,让首次加载时不去加载 math 模块,从而减少首次加载资源的体积。

import("./math").then(math => {
 console.log(math.add(16, 26));
});

2、例如引用react的高阶组件react-loadable进行动态import。

import Loadable from 'react-loadable';
import Loading from './loading-component';

const LoadableComponent = Loadable({
 loader: () => import('./my-component'),
 loading: Loading,
});

export default class App extends React.Component {
 render() {
  return <LoadableComponent/>;
 }
}

上面的代码在首次加载时,会先展示一个 loading-component,然后动态加载 my-component 的代码,组件代码加载完毕之后,便会替换掉 loading-component

shouldComponentUpdate避免重复渲染

当一个组件的props或者state改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的DOM。当他们不相等时,React会更新DOM。

在一些情况下,你的组件可以通过重写这个生命周期函数shouldComponentUpdate来提升速度, 它是在重新渲染过程开始前触发的。 这个函数默认返回true,可使React执行更新。

为了进一步说明问题,引用官网的图解释一下,如下图( SCU表示shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):

浅谈react性能优化的方法

根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

  • C1根节点,绿色SCU、红色vDOMEq,表示需要更新。
  • C2节点,红色SCU,表示不需要更新,同时
  • C4、C5作为其子节点也不需要检查更新。
  • C3节点,绿色SCU、红色vDOMEq,表示需要更新。
  • C6节点,绿色SCU、红色vDOMEq,表示需要更新。
  • C7节点,红色SCU,表示不需要更新。
  • C8节点,绿色SCU,表示React需要渲染这个组件;绿色vDOMEq,表示虚拟DOM一致,不更新DOM。

因此,我们可以通过根据自己的业务特性,重载shouldComponentUpdate,只在确认真实DOM需要改变时,再返回true。一般的做法是比较组件的props和state是否真的发生变化,如果发生变化则返回true,否则返回false。引用官网的案例。

class CounterButton extends React.Component {
 constructor(props) {
  super(props);
  this.state = {count: 1};
 }

 shouldComponentUpdate(nextProps, nextState) {
  if (this.props.color !== nextProps.color) {
   return true;
  }
  if (this.state.count !== nextState.count) {
   return true;
  }
  return false;
 }

 render() {
  return (
   <button
    color={this.props.color}
    onClick={() => this.setState(state => ({count: state.count + 1}))}>
    Count: {this.state.count}
   </button>
  );
 }
}

在以上代码中,shouldComponentUpdate只检查props.color和state.count的变化。如果这些值没有变化,组件就不会更新。当你的组件变得更加复杂时,你可以使用类似的模式来做一个“浅比较”,用来比较属性和值以判定是否需要更新组件。这种模式十分常见,因此React提供了一个辅助对象来实现这个逻辑 - 继承自React.PureComponent。

大部分情况下,你可以使用React.PureComponent而不必写你自己的shouldComponentUpdate,它只做一个浅比较。但是当你比较的目标为引用类型数据,浅比较会忽略属性或状态突变的情况,此时你不能使用它,此时你需要关注下面的不可突变数据。

附:数据突变(mutated)是指变量的引用没有改变(指针地址未改变),但是引用指向的数据发生了变化(指针指向的数据发生变更)。例如const x = {foo:'foo'}。x.foo='none' 就是一个突变。

使用不可突变数据结构

引用官网中的例子解释一下突变数据产生的问题。例如,假设你想要一个ListOfWords组件来渲染一个逗号分隔的单词列表,并使用一个带了点击按钮名字叫WordAdder的父组件来给子列表添加一个单词。以下代码并不正确:

class ListOfWords extends React.PureComponent {
 render() {
  return <div>{this.props.words.join(',')}</div>;
 }
}

class WordAdder extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   words: ['marklar']
  };
  this.handleClick = this.handleClick.bind(this);
 }

 handleClick() {
  // 这段内容将会导致代码不会按照你预期的结果运行
  const words = this.state.words;
  words.push('marklar');
  this.setState({words: words});
 }

 render() {
  return (
   <div>
    <button onClick={this.handleClick} />
    <ListOfWords words={this.state.words} />
   </div>
  );
 }
}

导致代码无法正常工作的原因是 PureComponent 仅仅对 this.props.words的新旧值进行“浅比较”。在words值在handleClick中被修改之后,即使有新的单词被添加到数组中,但是this.props.words的新旧值在进行比较时是一样的(引用对象比较),因此 ListOfWords 一直不会发生渲染。
避免此类问题最简单的方式是避免使用值可能会突变的属性或状态,如:

1、数组使用concat,对象使用Object.assign()

handleClick() {
 this.setState(prevState => ({
  words: prevState.words.concat(['marklar'])
 }));
}
// 假设我们有一个叫colormap的对象,下面方法不污染原始对象
function updateColorMap(colormap) {
 return Object.assign({}, colormap, {right: 'blue'});
}

2、ES6支持数组或对象的spread语法

handleClick() {
 this.setState(prevState => ({
  words: [...prevState.words, 'marklar'],
 }));
};
function updateColorMap(colormap) {
 return {...colormap, right: 'blue'};
}

3、使用不可突变数据immutable.js

immutable.js使得变化跟踪很方便。每个变化都会导致产生一个新的对象,因此我们只需检查索引对象是否改变。

const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
x === y; // false

在这个例子中,x突变后返回了一个新的索引,因此我们可以安全的确认x被改变了。

不可突变的数据结构帮助我们轻松的追踪对象变化,从而可以快速的实现shouldComponentUpdate。

具体如何使用可参考下面文章:Immutable 详解及 React 中实践

组件尽可能的进行拆分、解耦

组件尽可能的细分,比如一个input+list组件,可以将list分成一个PureComponent,只在list数据变化时更新。否则在input值变化页面重新渲染的时候,list也需要进行不必要的DOM diff。

列表类组件优化

key属性在组件类之外提供了另一种方式的组件标识。通过key标识,在组件发生增删改、排序等操作时,可以根据key值的位置直接调整DOM顺序,告诉React 避免不必要的渲染而避免性能的浪费。
例,对于一个基于排序的组件渲染:

var items = sortBy(this.state.sortingAlgorithm, this.props.items);
return items.map(function(item){
 return <img src={item.src} />
});

当顺序发生改变时,React 会对元素进行diff操作,并改img的src属性。显示,这样的操作效率是非常低的。这时,我们可以为组件添加一个key属性以唯一的标识组件:

return <img src={item.src} key={item.id} />

增加key后,React就不是diff,而是直接使用insertBefore操作移动组件位置,而这个操作是移动DOM节点最高效的办法。

bind函数

绑定this的方式:一般有下面3种方式:

1、constructor绑定

constructor(props) {
  super(props);
  this.handleClick = this.handleClick.bind(this); //构造函数中绑定
}
//然后可以
<p onClick={this.handleClick}>

2、使用时绑定

<p onClick={this.handleClick.bind(this)}>

3、使用箭头函数

<Test click={() => { this.handleClick() }}/>

以上三种方法,第一种最优。

因为第一种构造函数只在组件初始化的时候执行一次,

第二种组件每次render都会执行

第三种在每一次render时候都会生成新的箭头函数。例:Test组件的click属性是个箭头函数,组件重新渲染的时候Test组件就会

因为这个新生成的箭头函数而进行更新,从而产生Test组件的不必要渲染。

不要滥用props

props尽量只传需要的数据,避免多余的更新,尽量避免使用{...props}

ReactDOMServer进行服务端渲染组件

为了用户会更快速地看到完整渲染的页面,可以采用服务端渲染技术,在此了解一下ReactDOMServer。

为了实现SSR,你可能会用nodejs框架(Express、Hapi、Koa)来启动一个web服务器,接着调用 renderToString 方法去渲染你的根组件成为字符串,最后你再输出到 response。

// using Express
import { renderToString } from "react-dom/server";
import MyPage from "./MyPage";
app.get("/", (req, res) => {
 res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
 res.write("<div id='content'>"); 
 res.write(renderToString(<MyPage/>));
 res.write("</div></body></html>");
 res.end();
});

客户端使用render方法来生成HTML

import ReactDOM from 'react-dom';
import MyPage from "./MyPage";
ReactDOM.render(<MyPage />, document.getElementById('app'));

react性能检测工具

react16版本之前,我们可以使用react-addons-perf工具来查看,而在最新的16版本,我们只需要在url后加上?react_pref。
首先来了解一下react-addons-perf。

react-addons-perf这是 React 官方推出的一个性能工具包,可以打印出组件渲染的时间、次数、浪费时间等。
简单说几个api,具体用法可参考官网:

  • Perf.start() 开始记录
  • Perf.stop() 结束记录
  • Perf.printInclusive() 查看所有设计到的组件render
  • Perf.printWasted() 查看不需要的浪费组件render

再来了解一下,react16版本的方法,在url后加上?react_pref,就可以在chrome浏览器的performance,我们可以查看User Timeing来查看组件的加载时间。点击record开始记录,注意记录时长不要超过20s,否则可能导致chrome挂起。

浅谈react性能优化的方法

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
特殊字符、常规符号及其代码对照表
Jun 26 Javascript
神奇的7个jQuery 3D插件整理
Jan 06 Javascript
jquery foreach使用示例
Sep 12 Javascript
JQuery中操作Css样式的方法
Feb 12 Javascript
浅谈JavaScript的Polymer框架中的事件绑定
Jul 29 Javascript
Angular发布1.5正式版,专注于向Angular 2的过渡
Feb 18 Javascript
浅析JS动态创建元素【两种方法】
Apr 20 Javascript
Bootstrap导航条学习使用(二)
Feb 08 Javascript
JS设计模式之观察者模式实现实时改变页面中金额数的方法
Feb 05 Javascript
vue动画打包后失效问题的解决方法
Sep 18 Javascript
vue 开发企业微信整合案例分析
Dec 02 Javascript
js实现随机点名
Jan 19 Javascript
angularjs性能优化的方法
Sep 05 #Javascript
jQuery实现获取及设置CSS样式操作详解
Sep 05 #jQuery
在vue中给列表中的奇数行添加class的实现方法
Sep 05 #Javascript
Vue.js 实现数据展示全部和收起功能
Sep 05 #Javascript
浅谈vue 单文件探索
Sep 05 #Javascript
快速解决vue动态绑定多个class的官方实例语法无效的问题
Sep 05 #Javascript
jQuery扩展方法实现Form表单与Json互相转换的实例代码
Sep 05 #jQuery
You might like
锁定年轻人的双倍活力 星巴克推出星倍醇即饮浓咖啡
2021/03/03 咖啡文化
php 应用程序安全防范技术研究
2009/09/25 PHP
php设计模式 Facade(外观模式)
2011/06/26 PHP
php微信浏览器分享设置以及回调详解
2016/08/01 PHP
父子窗体间传递JSON格式的数据的代码
2010/12/25 Javascript
鼠标事件延时切换插件
2011/03/12 Javascript
FusionCharts图表显示双Y轴双(多)曲线
2012/11/22 Javascript
JQuery下拉框应用示例介绍
2014/04/23 Javascript
js中的如何定位固定层的位置
2014/06/15 Javascript
Node.js中的流(Stream)介绍
2015/03/30 Javascript
jquery实现的动态回到顶部特效代码
2015/10/28 Javascript
jQuery模仿京东/天猫商品左侧分类导航菜单效果
2016/06/29 Javascript
js模式化窗口问题![window.dialogArguments]
2016/10/30 Javascript
nodejs进阶(6)—连接MySQL数据库示例
2017/01/07 NodeJs
VUE饿了么树形控件添加增删改功能的示例代码
2017/10/17 Javascript
浅谈Webpack 持久化缓存实践
2018/03/22 Javascript
vue 使用鼠标滚动加载数据的例子
2019/10/31 Javascript
微信公众号H5之微信分享常见错误和问题(小结)
2019/11/14 Javascript
js实现打字小游戏
2019/12/17 Javascript
Node.js学习之内置模块fs用法示例
2020/01/22 Javascript
Vue实现一种简单的无限循环滚动动画的示例
2021/01/10 Vue.js
[04:47]DOTA2-潍坊风行电子俱乐部探秘
2014/08/08 DOTA
Python编程中的文件操作攻略
2015/10/16 Python
Django实现登录随机验证码的示例代码
2018/06/20 Python
python:接口间数据传递与调用方法
2018/12/17 Python
Python用字典构建多级菜单功能
2019/07/11 Python
Python使用monkey.patch_all()解决协程阻塞问题
2020/04/15 Python
美国婚礼装饰和活动用品批发供应商:Event Decor Direct
2018/10/12 全球购物
黄色火烈鸟:De Gele Flamingo
2019/03/18 全球购物
Craghoppers德国官网:户外和旅行服装
2020/02/14 全球购物
金山毒霸系列的笔试题
2013/04/13 面试题
手机业务员岗位职责
2013/12/13 职场文书
党校培训思想汇报
2013/12/30 职场文书
中国梦演讲稿开场白
2014/08/28 职场文书
2015年行政管理人员工作总结
2015/10/15 职场文书
MySQL 逻辑备份与恢复测试的相关总结
2021/05/14 MySQL