React 性能优化
React 应用也是前端应用,前端项目普适的性能优化手段,比如资源加载过程中的优化、减少重绘与回流、服务端渲染、启用 CDN 等,这些手段对于 React 来说也是同样奏效的。
但是 React 区别于传统的前端项目特点就是 以组件的形式来组织代码,React 允许我们把一个大的 UI 界面,拆分出可复用的代码片段(也就是组件),可以对每一部分的代码片段进行独立构思。因此,React 组件有一些特色的性能优化思路。接下来,我们开始讲解 React 性能优化的思路!
React 组件性能优化的手段,主要有三种:
- 使用 shouldComponentUpdate 来阻止不必要的子组件的 ‘re-render’(重渲染)
- PureComponent + immutable.js
- React.memo 与 useMemo
1. 基于 shouldComponentUpdate 优化
render 方法由于伴随着对虚拟 DOM 的构建和对比,这其中的过程非常耗时,而在 React 中,很多时候会不经意的调用 ‘re-render’,为了避免不必要的 render 带来的性能消耗,因此出现了 shuouldComponentUpdate 这个生命周期。
shouldComponentUpdate 是 React 更新阶段的一个生命周期,React 组件会根据 shouldComponentUpdate 这个生命周期的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行 ‘re-render’(重渲染)
接下来直接代码展示
分别创建 home, childA , childB 三个组件(home是主页面,childA, childB分别是它的子组件)
home.jsx
import React, { Component } from 'react'
import ChildA from "./childA"
import ChildB from "./childB"
export default class Home extends Component {
state = {
childA: '文本A',
childB: '文本B',
}
changeA = () => {
this.setState({
childA: '改变文本A'
})
}
changeB = () => {
this.setState({
childB: '改变文本B'
})
}
render() {
const {childA, childB} = this.state
return (
<div>
<button onClick={this.changeA}>改变childA的文本</button>
<button onClick={this.changeB}>改变childB的文本</button>
<ChildA textA={childA}></ChildA>
<ChildB textB={childB}></ChildB>
</div>
)
}
}
childA.jsx
import React, { Component } from 'react'
export default class ChildA extends Component {
render() {
console.log('childA 被 render')
return (
<div>
<h3>A组件文本:{this.props.textA}</h3>
</div>
)
}
}
childB.jsx
import React, { Component } from 'react'
export default class ChildB extends Component {
render() {
console.log('childB 被 render')
return (
<div>
<h3>B组件文本:{this.props.textB}</h3>
</div>
)
}
}
效果如下:
组件初始化时:打印台输出,childA. childB 都被render
点击 改变childA文本 按钮时:打印台输出, childA, childB 也都被改变
上图所示,组件初始化 不加 shouldComponentUpdate 情况下,当给 childB.jsx 加上shouldComponentUpdate之后
import React, { Component } from 'react'
export default class ChildB extends Component {
shouldComponentUpdate(nextProps, nextState) {
// 判断 text 属性在父组件更新前后有没有发生变化,若没有发生变化,则返回 false
if(nextProps.textB === this.props.textB) {
return false
}
// 只有在 text 属性值确实发生变化时,才允许更新进行下去
return true
}
render() {
console.log('childB 被 render')
return (
<div>
<h3>B组件文本:{this.props.textB}</h3>
</div>
)
}
}
效果:
打印台输出, 只有 childA 被render
因此我们得出结论,在 React 中,只要父组件发生了更新,那么所有的子组件都会被无条件的更新,这就导致了无论 props 是否改变,子组件都会被重新 render。
2. 基于 PureComponent + immutable.js 优化
pureComponent 是在 shouldComponentUpdate 衍生出来的玩法。它的功能也是避免 re-render。
PureComponent 与 Component 的区别点,就在于它内置了对 shouldComponentUpdate 的实现:PureComponent 将会在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。
“浅比较”将针对值类型数据对比其值是否相等,而针对数组、对象等引用类型的数据则对比其引用是否相等。
这次把 childB.jsx 修改成 PureComponent, 代码如下:
import React from "react";
export default class ChildB extends React.PureComponent {
render() {
console.log("ChildB 的render方法执行了");
return (
<div className="childB">
子组件B的内容:
{this.props.text}
</div>
);
}
}
实现了和 shouldComponentUpdate 一样的效果。
但是,PureComponent 如果数据类型为引用类型,那么这种基于浅比较的判断逻辑就会带来这样两个风险:
- 若数据内容没变,但是引用变了,那么浅比较仍然会认为“数据发生了变化”,进而触发一次不必要的更新,导致过度渲染;
- 若数据内容变了,但是引用没变,那么浅比较则会认为“数据没有发生变化”,进而阻断一次更新,导致不渲染。
解决方法 Immutable
Immutable 它的意思就是 “持久性数据”,指的是这个数据只要被创建出来了,就不能被更改。我们对当前数据的任何修改动作,都会导致一个新的对象的返回。这就将数据内容的变化和数据的引用严格地关联了起来,侦听到所有的变化。
举个栗子:
// 引入 immutable 库里的 Map 对象,它用于创建对象
import { Map } from 'immutable'
// 初始化一个对象 baseMap
const baseMap = Map({
name: '小明',
age: 99
})
// 使用 immutable 暴露的 Api set 修改 baseMap 的内容
const changedMap = baseMap.set({
age: 100
})
// 我们会发现修改 baseMap 后将会返回一个新的对象,这个对象的引用和 baseMap 是不同的
console.log(baseMap === changedMap) // false
3. 基于 React.memo 和 useMemo 函数组件性能优化
React.memo
相当于函数版 shouldComponentUpdate / Purecomponent,它是 React 导出的一个顶层函数,它的本质是一个高阶组件,负责对函数组件进行包装。
使用方法如下:
import React from "react";
// 定义一个函数组件
function Home(props) {
return xxx
}
// compare 函数是 memo 的第二个入参,我们之前放在 shouldComponentUpdate 里面的逻辑就可以转移至此处
function compare(prevProps, nextProps) {
if(preProps === nextProps) {
return false
}
return true
}
// 使用 React.memo 来包装函数组件
export default React.memo(Home, compare);
从示例中我们可以看出,React.memo 接收两个参数,第一个参数是我们需要渲染的目标组件,第二个参数 compare则用来承接 props 的对比逻辑。之前我们在 shouldComponentUpdate 里面做的事情,现在就可以放在 compare里来做。
把 childA 优化:
import React from "react"
// 将 ChildA 改写为 function 组件
function ChildB(props) {
console.log("ChildA 被 render");
return (
<div>
<h3>A组件文本:{props.textA}</h3>
</div>
)
}
// compare 用于对比 props 的变化,只有当相同的时候返回 true 开启 React.memo
function compare(prevProps, nextProps) {
if(nextProps.textA === prevProps.textA) {
return true
}
return false
}
// 使用 React.memo 来包装 ChildB
export default React.memo(ChildB, compare);
这里的 compare 函数是一个可选参数,当我们不传入 compare 时,React.memo 也可以工作,此时它的作用就类似于 PureComponent——React.memo 会自动为你的组件执行 props 的浅比较逻辑。
和 shouldComponentUpdate 不同的是,React.memo 只负责对比 props,而不会去感知组件内部状态(state)的变化。
useMemo
useMemo 是一种更加精细的 Memo,React.memo 可以实现类似于 shouldComponentUpdate 或者 PureComponent 的效果,对组件级别的 re-render 进行管控。但是有时候,我们希望复用的并不是整个组件,而是组件中的某一个或几个部分,就需要 useMemo 来帮忙了。
因此,React.memo 控制是否需要重渲染一个组件,而 useMemo 控制的则是是否需要重复执行某一段逻辑。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
我们可以把目标逻辑作为第一个参数传入,把逻辑的依赖项数组作为第二个参数传入。这样只有当依赖项数组中的某个依赖发生变化时,useMemo 才会重新执行第一个入参中的目标逻辑。
新加一个 childC.jsx:(使用useMemo)
import React, { useMemo } from 'react'
function ChildC({text, num}) {
console.log('childC 被 render');
const renderText = (text) => {
console.log('childC renderText执行了');
return <span>{text}</span>
}
const renderNum = (num) => {
console.log('childC renderNum执行了');
return <span>{num}</span>
}
const textMemo = useMemo(() => renderText(text), [text])
const numMemo = useMemo(() => renderNum(num), [num])
return (
<div>
<h3>child3内容是:{textMemo}</h3>
<h3>child3数字是:{numMemo}</h3>
</div>
)
}
export default ChildC
同时修改 home.jsx
import React, { Component } from 'react'
import ChildA from "./childA"
import ChildB from "./childB"
import ChildC from "./childC"
export default class Home extends Component {
state = {
childA: '文本A',
childB: '文本B',
childC: {
text: '文本C',
num: 10
}
}
changeA = () => {
this.setState({
childA: '改变文本A'
})
}
changeB = () => {
this.setState({
childB: '改变文本B'
})
}
changeC = () => {
this.setState({
childC: {
...this.state.childC,
num: 100
}
})
}
render() {
const {childA, childB, childC} = this.state
return (
<div>
<button onClick={this.changeA}>改变childA的文本</button>
<button onClick={this.changeB}>改变childB的文本</button>
<button onClick={this.changeC}>改变childC的文本</button>
<ChildA textA={childA}></ChildA>
<ChildB textB={childB}></ChildB>
<ChildC {...childC}></ChildC>
</div>
)
}
}
效果如下图所示:
效果如图所示,检测到了只改变了childC中的num,所以组件 ChildC 仅仅 render 了 num , 没有重新render text。
使用 useMemo,我们可以对函数组件的执行进行更加精细的管控(尤其是定向规避掉一些高开销的计算),同时也弥补了 React.memo 无法感知函数内部状态变化的缺点。