浅谈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 相关文章推荐
再谈querySelector和querySelectorAll的区别与联系
Apr 20 Javascript
jquery创建一个ajax关键词数据搜索实现思路
Feb 26 Javascript
jQuery实现简单的图片查看器
Sep 11 Javascript
js基于cookie方式记住返回页面用法示例
May 27 Javascript
Javascript实现跑马灯效果的简单实例
May 31 Javascript
JavaScript &amp; jQuery完美判断图片是否加载完毕
Jan 08 Javascript
微信小程序如何获取用户收货地址
Nov 27 Javascript
[原创]微信小程序获取网络类型的方法示例
Mar 01 Javascript
小程序实现搜索框
Jun 19 Javascript
node.js express捕获全局异常的三种方法实例分析
Dec 27 Javascript
Vue Element校验validate的实例
Sep 21 Javascript
如何用vue实现网页截图你知道吗
Nov 17 Vue.js
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
数据库相关问题
2006/10/09 PHP
PHP禁止个别IP访问网站
2013/10/30 PHP
PHP根据IP判断地区名信息的示例代码
2014/03/03 PHP
windows7下安装php的php-ssh2扩展教程
2014/07/04 PHP
PHP中require和include路径问题详解
2014/12/25 PHP
Mac系统下安装PHP Xdebug
2018/03/30 PHP
PHP后台实现微信小程序登录
2018/08/03 PHP
Laravel等框架模型关联的可用性浅析
2019/12/15 PHP
学习js所必须要知道的一些
2007/03/07 Javascript
图片在浏览器中底部对齐 解决方法之一
2011/11/30 Javascript
AngularJS 简单应用实例
2016/07/28 Javascript
微信小程序 Template详解及简单实例
2017/01/05 Javascript
JS查找英文文章中出现频率最高的单词
2017/03/20 Javascript
vue.js默认路由不加载linkActiveClass问题的解决方法
2017/12/11 Javascript
vue和webpack打包项目相对路径修改的方法
2018/06/15 Javascript
angularJs中跳转到指定的锚点实例($anchorScroll)
2018/08/31 Javascript
区别JavaScript函数声明与变量声明
2018/09/12 Javascript
微信公众平台 客服接口发消息的实现代码(Java接口开发)
2019/04/17 Javascript
Vue实现导航栏的显示开关控制
2019/11/01 Javascript
使用python实现正则匹配检索远端FTP目录下的文件
2015/03/25 Python
Python爬虫抓取手机APP的传输数据
2016/01/22 Python
Python3.6安装及引入Requests库的实现方法
2018/01/24 Python
Python中文件的读取和写入操作
2018/04/27 Python
基于MTCNN/TensorFlow实现人脸检测
2018/05/24 Python
转换科学计数法的数值字符串为decimal类型的方法
2018/07/16 Python
如何使用Python自动控制windows桌面
2019/07/11 Python
django drf框架自带的路由及最简化的视图
2019/09/10 Python
使用HTML5的Canvas绘制曲线的简单方法
2015/09/08 HTML / CSS
中国最大的潮流商品购物网站:YOHO!BUY有货
2017/01/07 全球购物
托管代码(Managed Code)和非托管代码(Unmanaged Code)有什么区别
2014/09/29 面试题
舞蹈毕业生的自我评价
2014/03/05 职场文书
人事经理岗位职责范本
2014/08/04 职场文书
毕业生找工作求职信
2014/08/05 职场文书
校长个人总结
2015/03/03 职场文书
党小组意见范文
2015/06/08 职场文书
Python利用folium实现地图可视化
2021/05/23 Python