前言
目的
目前在工作中,大量的项目都是使用 react 来进行开展的,了解掌握下 React 的性能优化对项目的体验和可维护性都有很大的好处,下面介绍下在 React 中可以运用的一些性能优化方式;
性能优化思路
对于类式组件和函数式组件来看,都可以从以下几个方面去思考如何能够进行性能优化
减少重新 Render 的次数
减少渲染的节点
降低渲染计算量
合理设计组件
减少重新 Render 的次数
在 React 里时间耗时多的一个地方是 Reconciliation(reconciliation 的终目标是以有效的方式,根据新的状态来更新 UI,我们可以简单地理解为 diff),如果不执行 Render,也就不需要 Reconciliation,所以可以看出减少 Render 在性能优化过程中的重要程度了。
PureComponent
React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 Prop 和 State 的方式来实现了该函数。
需要注意的是在使用 PureComponent 的组件中,在 Props 或者 State 的属性值是对象的情况下,并不能阻止不必要的渲染,是因为自动加载的 shouldComponentUpdate 里面做的只是浅比较,所以想要用 PureComponent 的特性,应该遵守原则:
确保数据类型是值类型
如果是引用类型,不应当有深层次的数据变化(解构)
ShouldComponentUpdate
可以利用此事件来决定何时需要重新渲染组件。如果组件 Props 更改或调用 setState,则此函数返回一个 Boolean 值,为 true 则会重新渲染组件,反之则不会重新渲染组件。
在这两种情况下组件都会重新渲染。我们可以在这个生命周期事件中放置一个自定义逻辑,以决定是否调用组件的 Render 函数。
下面举一个小的例子来辅助理解下:
比如要在你的应用中展示学生的详细资料,每个学生都包含有多个属性,如姓名、年龄、爱好、身高、体重、家庭住址、父母姓名等;在这个组件场景中,只需要展示学生的姓名、年龄、住址,其他的信息不需要在这里展示,所以在理想情况下,除去姓名、年龄、住址以外的信息变化组件是不需要重新渲染的;
示例代码如下:
import React from"react";
exportdefaultclassShouldComponentUpdateUsageextendsReact.Component{
constructor(props) {
super(props);
this.state = {
name: "小明",
age: 12,
address: "xxxxxx",
height: 165,
weight: 40 }
}
componentDidMount() {
setTimeout(() => {
this.setState({
height: 168,
weight: 45 });
}, 5000)
}
shouldComponentUpdate(nextProps, nextState) {
if(nextState.name !== this.state.name || nextState.age !== this.state.age || nextState.address !== this.state.address) {
returntrue;
}
returnfalse;
}
render() {
const { name, age, address } = this.state;
return (
<div><p>Student name: {name} </p><p>Student age:{age} </p><p>Student address:{address} </p></div> )
}
}
按照 React 团队的说法,shouldComponentUpdate 是保证性能的紧急出口,既然是紧急出口,那就意味着我们轻易用不到它。但既然有这样一个紧急出口,那说明有时候它还是很有必要的。所以我们要搞清楚到底什么时候才需要使用这个紧急出口。
使用原则
当你觉得,被改变的 State 或者 Props,不需要更新视图时,你就应该思考要不要使用它。
需要注意的一个地方是:改变之后,又不需要更新视图的状态,也不应该放在 State 中。
shouldComponentUpdate 的使用,也是有代价的。如果处理得不好,甚至比多 Render 一次更消耗性能,另外也会使组件的复杂度增大,一般情况下使用PureComponent即可;
React.memo
如果你的组件在相同 Props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用近一次渲染的结果。
React.memo 仅检查 Props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 State 或 Context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
functionMyComponent(props) {
/* 使用 props 渲染 */}
functionareEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/}
exportdefault React.memo(MyComponent, areEqual);
注意:与 Class 组件中 shouldComponentUpdate() 方法不同的是,如果 Props 相等,areEqual 会返回 true;如果 Props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。
合理使用 Context
Context 提供了一个无需为每层组件手动添加 Props,就能在组件树间进行数据传递的方法。正是因为其这个特点,它是可以穿透 React.memo 或者 shouldComponentUpdate 的比对的,也就是说,一旦 Context 的 Value 变动,所有依赖该 Context 的组件会全部 forceUpdate.这个和 Mobx 和 vue 的响应式系统不同,Context api 并不能细粒度地检测哪些组件依赖哪些状态。
原则
Context 中只定义被大多数组件所共用的属性,例如当前用户的信息、主题或者选择的语言。
避免使用匿名函数
首先来看下下面这段代码
const MenuContainer = ({ list }) => (
<Menu> {list.map((i) => (
<MenuItem key={i.id}onClick={() => handleClick(i.id)} value={i.value} />
))}
</Menu>);
上面这个写法看起来是比较简洁,但是有一个潜在问题是匿名函数在每次渲染时都会有不同的引用,这样就会导致 Menu 组件会出现重复渲染的问题;可以使用 useCallback 来进行优化:
const MenuContainer = ({ list }) => {
const handleClick = useCallback(
(id) => () => {
// ... },
[],
);
return (
<Menu> {list.map((i) => (
<MenuItem key={i.id}id={i.id}onClick={handleClick(i.id)}value={i.value} /> ))}
</Menu> );
};
减少渲染的节点
组件懒加载
组件懒加载可以让 React 应用在真正需要展示这个组件的时候再去展示,可以比较有效的减少渲染的节点数提高页面的加载速度
React 官方在 16.6 版本后引入了新的特性:React.lazy 和 React.Suspense,这两个组件的配合使用可以比较方便进行组件懒加载的实现;
React.lazy
该方法主要的作用就是可以定义一个动态加载的组件,这可以直接缩减打包后 bundle 的体积,并且可以延迟加载在初次渲染时不需要渲染的组件,代码示例如下:
使用之前
import SomeComponent from'./SomeComponent';
使用之后
const SomeComponent = React.lazy(() => import('./SomeComponent'));
使用 React.lazy 的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 Polyfill 来使用该特性。
React.Suspense
该组件目前主要的作用就是配合渲染 lazy 组件,这样就可以在等待加载 lazy组件时展示 loading 元素,不至于直接空白,提升用户体验;
Suspense 组件中的 fallback 属性接受任何在组件加载过程中你想展示的 React 元素。
你可以将 Suspense 组件置于懒加载组件之上的任何位置,你甚至可以用一个 Suspense 组件包裹多个懒加载组件。
代码示例如下:
import React, { Suspense } from'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
functionMyComponent() {
return (
<div><Suspense fallback={<div>Loading...</div>}>
<section><OtherComponent /><AnotherComponent /></section></Suspense></div> );
}
有一点要特别注意的是:React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,推荐使用 Loadable Components 这个库,可以结合这个文档服务端渲染打包指南来进行查看。
另外在业内也有一些比较成熟的 React 组件懒加载开源库:react-loadable 和react-lazyload,感兴趣的可以结合看下;
虚拟列表
虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术,在开发一些项目中,会遇到一些不是直接分页来加载列表数据的场景,在这种情况下可以考虑结合虚拟列表来进行优化,可以达到根据容器元素的高度以及列表项元素的高度来显示长列表数据中的某一个部分,而不是去完整地渲染长列表,以提高无限滚动的性能。
可以关注下放两个比较常用的类库来进行深入了解
react-virtualized
react-window
降低渲染计算量
useMemo
先来看下 useMemo 的基本使用方法
functioncomputeExpensiveValue(a, b) {
// 计算量很大的一些逻辑return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组里面的值有变化,那么就会重新去执行个参数里面的函数,并将函数返回的值缓存起来并作为 useMemo 的返回值 。
注意
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值;
计算量如果很小的计算函数,也可以选择不使用 useMemo,因为这点优化并不会作为性能瓶颈的要点,反而可能使用错误还会引起一些性能问题。
遍历展示视图时使用 key
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) => <li key={number.toString()}> {number}
</li>);
使用 key 注意事项:
好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key,当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key
元素的 key 只有放在就近的数组上下文中才有意义。例如,如果你提取出一个 ListItem 组件,你应该把 key 保留在数组中的这个 元素上,而不是放在 ListItem 组件中的元素上。
合理设计组件
简化 Props
如果一个组件的 Props 比较复杂的话,会影响 shallowCompare 的效率,也会使这个组件变得难以维护,另外也与“单一职责”的原则不符合,可以考虑进行拆解。
简化 State
在设计组件的 State 时,可以按照这个原则来:需要组件响应它的变动或者需要渲染到视图中的数据,才放到 State 中;这样可以避免不必要的数据变动导致组件重新渲染。
减少组件嵌套
一般不必要的节点嵌套都是滥用高阶组件/ RenderProps 导致的。所以还是那句话‘只有在必要时才使用 xxx’。有很多种方式来代替高阶组件/ RenderProps,例如优先使用 Props、React Hooks。