浅谈react性能优化的方法
作者:havywu
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比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):
根据渲染流程,首先会判断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挂起。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。