写点什么

React 性能优化

  • 2021 年 11 月 22 日
  • 本文字数:4649 字

    阅读完需:约 15 分钟

React性能优化

前言

React 是 Facebook 开发的构建用户界面的类库. 它从设计之初就将性能作为重点,在使用时更是可以采取一些策略然后我们网站性能更加优化,以下是我平时用到的一些优化方式,希望可以帮助到大家!

Code Splitting

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

1.入口起点:使用 entry 配置手动地分离代码。

2.防止重复:使用 SplitChunks 去重和分离 chunk。

3.动态导入:通过模块的内联函数调用来分离代码。

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

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。

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

组件尽可能的细分,比如一个 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 Expressimport { 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 挂起。

 


最后

如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star: https://gitee.com/ZhongBangKeJi/CRMEB不胜感激 !

用户头像

还未添加个人签名 2021.11.02 加入

CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用

评论

发布
暂无评论
React性能优化