浅谈react性能优化的方法

这篇文章主要介绍了浅谈react性能优化的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

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比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):

image
  • 根据渲染流程,首先会判断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挂起。

  • 以上就是本文的全部内容,希望对大家的学习有所帮助。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容