跳到主要内容

性能优化

React 为高性能应用设计提供了许多优化方案.

以下场景中, 父组件和子组件通常会重新渲染:

  1. 从同一组件或父组件中调用setState时.
  2. 从父级收到的props的值发生变化
  3. 调用组件中的 forceUpdate

0. React Profiler

React.Profiler 是 React 提供的,分析组件渲染次数、开始时间及耗时的一个 API,你可以在官网找到它的文档

当然我们不需要每个组件都去加一个 React.Profiler 包裹,在开发环境下,React 会默记录每个组件的信息,我们可以通过 Chrome Profiler Tab整体分析。

以下是一些提升 React 应用性能的技巧:

1. 使用纯组件

如果组件对于相同的状态和 props 会渲染相同的输出, 就可以将其视为纯组件.

对于像这样的类组件来说, React 提供了PureComponent基类来扩展纯组件.

PureComponents 与不同组件的区别在于会包含一个浅层比较的shouldComponentUpdate, 既是说在propsstate改变的时候, PureComponent将会对propsstate进行浅比较, 如果值和引用相同, 则不会更新组件.

比较基元和对象引用的开销比更新组件视图要低。因此查找状态和 props 值的变化会比不必要的更新更快.

1.1 不要在 render 的函数中绑定值

<CommentItem likeComment={() => this.likeComment(user.id)} />

这样的写法会导致每次父组件render方法被调用的时候都会创建一个新的函数, 将其传入likComment, 这会改变每一个子组件的props从而导致子组件的重新渲染(即使数据本身没有发生变化).

正确的应当是将函数的引用传递给子组件即可:

<CommentItem likeComment={this.likeComment} userID={user.id} />

1.2 不要在 render 方法里派生数据

render() {
const { posts } = this.props
const topTen = posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
return //...
}

每次组件重新渲染topTen的时候都将有一个新的引用, 即使posts没有改变并且派生数据也是一样的, 这会造成列表不必要的重新渲染.

正确的写法应当是缓存我们的派生数据来解决这个问题:

componentWillMount() {
this.setTopTenPosts(this.props.posts)
}
componentWillReceiveProps(nextProps) {
if (this.props.posts !== nextProps.posts) {
this.setTopTenPosts(nextProps)
}
}
setTopTenPosts(posts) {
this.setState({
topTen: posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
})
}

和其他生命周期事件不一样的是,我们的核心原则是将 render() 函数作为纯函数。

纯函数意味着我们应该确保 setState 和查询原生 DOM 元素等任何可以修改应用状态的东西不会被调用。

该函数永远不该更新应用的状态。

2. 善用记忆和缓存

2.1 使用 React.memo 进行组件记忆

React.memo 是一个高阶组件。它类似于PureComponent, 是类似原理的函数组件实现.

如果输入props相同则会跳过组件渲染从而提升性能.

但是可以为这个组件传递自定义的比较逻辑, 可以用自定义逻辑深度对比对象, 如果比较函数返回false则重新渲染组件, 否则就不会重新渲染.

function CustomisedComponen(props) {
return (
<div>
<b>User name: {props.name}</b>
<b>User age: {props.age}</b>
<b>User designation: {props.designation}</b>
</div>
);
}

function userComparator(previosProps, nextProps) {
if (
previosProps.user.name == nextProps.user.name ||
previosProps.user.age == nextProps.user.age ||
previosProps.user.designation == nextProps.user.designation
) {
return false;
} else {
return true;
}
}

// 下面的组件是默认组件的优化版本
// 如果“name”属性的props值相同,则不会重新呈现组件。
var memoComponent = React.memo(CustomisedComponent, userComparator);

2.2 善用React.useMemo

React.useMemo是React内置的Hooks之一, 主要为了解决函数组件的频繁的render时, 无差别频繁触发无用计算, 一般可以作为性能优化的手段之一.

const App = (props)=>{
const [boolean, setBoolean] = useState(false);
const [start, setStart] = useState(0);

// 这是一个非常耗时的计算
const result = computeExpensiveFunc(start);
}

比如在这个例子中, computedExpensiceFunc是一个非常耗时的计算, 但是当我们触发setBoolean的时候, 组件会重新渲染, computeExpensiveFunc会执行一次, 这次执行没有意义. React.useMemo 就是为了解决这个问题诞生的,它可以指定只有当 start 变化时,才允许重新计算新的 result 。

const result = useMemo(()=>computeExpensiveFunc(start), [start]);

2.3 合理使用 React.useCallback

const OtherComponent = React.memo(()=>{
...
});

const App = (props)=>{
const [boolan, setBoolean] = useState(false);
const [value, setValue] = useState(0);

const onChange = (v)=>{
axios.post(`/api?v=${v}&state=${state}`)
}

return (
<div>
{/* OtherComponent 是一个非常昂贵的组件 */}
<OtherComponent onChange={onChange}/>
</div>
)
}

在这个例子中OtherComponent是一个非常昂贵的组件, 我们要避免无用的render, 虽然已经用memo包裹了, 但是在父组件每次处罚setBoolean的时候, OtherComponent任然会频繁的render.

因为父级组件 onChange 函数在每一次 render 时,都是新生成的,导致子组件浅比较失效。通过 React.useCallback,我们可以让 onChange 只有在 state 变化时,才重新生成。

const onChange = React.useCallback((v)=>{
axios.post(`/api?v=${v}&state=${state}`)
}, [state])

通过 useCallback 包裹后, boolean 的变化不会触发 OtherComponent ,只有 state 变化时,才会触发,可以避免很多无用的 OtherComponent 执行。

但是仔细想想, state变换其实也是没有必要触发OtherComponent, 我们只要保证onChange一定能访问到最新的state就可以避免state变化是触发OtherComponentrender:

const onChange = usePersistFn((v)=>{
axios.post(`/api?v=${v}&state=${state}`)
})

上面的例子,我们使用了 Umi Hooks 的 usePersistFn,它可以保证函数地址永远不会变化,无论何时, onChange 地址都不会变化,也就是无论何时, OtherComponent 都不会重新 render 了。

3. 使用 shouldComponentUpdate 生命周期事件

可以利用此事件来决定何时需要重新渲染组件。如果组件 props 更改或调用 setState,则此函数返回一个 Boolean 值。

在这两种情况下组件都会重新渲染。我们可以在这个生命周期事件中放置一个自定义逻辑,以决定是否调用组件的 render 函数。

这个函数将 nextState 和 nextProps 作为输入,并可将其与当前 props 和状态做对比,以决定是否需要重新渲染。

import React from "react";

export default class ShouldComponentUpdateUsage extends React.Component {

constructor(props) {
super(props);
this.state = {
name: "Mayank";
age: 30,
designation: "Architect";
}
}

componentDidMount() {
setTimeout(() => {
this.setState({
designation: "Senior Architect"
});
}
}

shouldComponentUpdate(nextProps, nextState) {
if(nextState.age != this.state.age || netState.name = this.state.name) {
return true;
}
return false;
}

render() {
return (
<div>
<b>User Name:</b> {this.state.name}
<b>User Age:</b> {this.state.age}
</div>
)
}
}

4. 懒加载组件

通过 webpack 进行代码拆分可以再原型是进行动态加载, 减少初始包大小.

为此我们使用 Suspense 和 lazy。

import React, { lazy, Suspense } from 'react';

export default class CallingLazyComponents extends React.Component {
render() {
var ComponentToLazyLoad = null;

if (this.props.name == 'Mayank') {
ComponentToLazyLoad = lazy(() => import('./mayankComponent'));
} else if (this.props.name == 'Anshul') {
ComponentToLazyLoad = lazy(() => import('./anshulComponent'));
}
return (
<div>
<h1>This is the Base User: {this.state.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<ComponentToLazyLoad />
</Suspense>
</div>
);
}
}

优点:

  1. 主包体积变小, 消耗的网络传输时间更少
  2. 动态单独加载的包比较小, 可以迅速加载完成

我们可以分析应用来决定懒加载那些组件, 从而减少应用的初始加载时间.

5. 使用 React Fragments 避免额外标记

使用 Fragments 减少了包含的额外标记数量,这些标记只是为了满足在 React 组件中具有公共父级的要求。

export default class NestedRoutingComponent extends React.Component {
render() {
return (
<>
<h1>This is the Header Component</h1>
<h2>Welcome To Demo Page</h2>
</>
);
}
}

上面的代码没有额外的标记,因此节省了渲染器渲染额外元素的工作量。

6. 避免 componentWillMount()中的异步请求

componentWillMount 是在渲染组件之前调用的。

这个函数用的不多,可用来配置组件的初始配置,但使用 constructor 方法自己也能做到。

该方法无法访问 DOM 元素,因为组件还没挂载上来。

一些开发人员认为这个函数可以用来做异步数据 API 调用,但其实这没什么好处。

由于 API 调用是异步的,因此组件在调用 render 函数之前不会等待 API 返回数据。于是在初始渲染中渲染组件时没有任何数据。

7. 在 Constructor 的早期绑定函数

当我们在 React 中创建函数时,我们需要使用 bind 关键字将函数绑定到当前上下文。

绑定可以在构造函数中完成,也可以在我们将函数绑定到 DOM 元素的位置上完成。

两者之间似乎没有太大差异,但性能表现是不一样的。

import React from 'react';

export default class DelayedBinding extends React.Component {
constructor() {
this.state = {
name: 'Mayank'
};
}

handleButtonClick() {
alert('Button Clicked: ' + this.state.name);
}

render() {
return (
<>
<input type="button" value="Click" onClick={this.handleButtonClick.bind(this)} />
</>
);
}
}

这样的写法问题在于每次调用 render 函数时都会创建并使用绑定到当前上下文的性函数, 更好的写法应当是在构造函数期间就直接绑定上下文:

import React from 'react';

export default class DelayedBinding extends React.Component {
constructor() {
this.state = {
name: 'Mayank'
};
this.handleButtonClick = this.handleButtonClick.bind(this);
}

handleButtonClick() {
alert('Button Clicked: ' + this.state.name);
}

render() {
return (
<>
<input type="button" value="Click" onClick={this.handleButtonClick} />
</>
);
}
}

8. 箭头函数与构造函数中的绑定

处理类时的标准做法就是使用箭头函数。使用箭头函数时会保留执行的上下文。

我们调用它时不需要将函数绑定到上下文。

import React from 'react';

export default class DelayedBinding extends React.Component {
constructor() {
this.state = {
name: 'Mayank'
};
}

handleButtonClick = () => {
alert('Button Clicked: ' + this.state.name);
};

render() {
return (
<>
<input type="button" value="Click" onClick={this.handleButtonClick} />
</>
);
}
}

箭头函数的优点是显而易见的, 但是也有缺点:

  1. 添加箭头函数时, 该函数被添加为对象实例而不是类的原型属性, 这意味着如果多次服用组件, 那么在组件外创建的每个对象中都会有这些函数的多个实例.
  2. 每个组件都会有这些函数的一份实例, 影响了可复用性. 此外因为它是对象属性而不是原型属性, 所以这些函数在继承链中不可用.

因此箭头函数确实有其缺点。实现这些函数的最佳方法是在构造函数中绑定函数,如上所述。

9. 避免使用内联样式属性

使用内联样式时浏览器需要花费更多时间来处理脚本和渲染,因为它必须映射传递给实际 CSS 属性的所有样式规则。

import React from 'react';

export default class InlineStyledComponents extends React.Component {
render() {
return (
<>
<b style={{ backgroundColor: 'blue' }}>Welcome to Sample Page</b>
</>
);
}
}

更好的办法是将 CSS 文件导入组件。

10. 优化 React 中的条件渲染

安装和卸载 React 组件是昂贵的操作。为了提升性能,我们需要减少安装和卸载的操作。

很多情况下在我们可能会渲染或不渲染特定元素,这时可以用条件渲染。

import React from 'react';

import AdminHeaderComponent from './AdminHeaderComponent';
import HeaderComponent from './HeaderComponent';
import ContentComponent from './ContentComponent';

export default class ConditionalRendering extends React.Component {
constructor() {
this.state = {
name: 'Mayank'
};
}

render() {
if (this.state.name == 'Mayank') {
return (
<>
<AdminHeaderComponent />
<HeaderComponent />
<ContentComponent />
</>
);
} else {
return (
<>
<HeaderComponent />
<ContentComponent />
</>
);
}
}
}

React 将观察元素的位置。它看到位置 1 和位置 2 的组件已更改并将卸载组件。

组件 HeaderComponent 和 ContentComponent 将在位置 1 和位置 2 卸载并重新安装。其实这是用不着的,因为这些组件没有更改,这是一项昂贵的操作。优化方案如下:

import React from 'react';

import AdminHeaderComponent from './AdminHeaderComponent';
import HeaderComponent from './HeaderComponent';
import ContentComponent from './ContentComponent';

export default class ConditionalRendering extends React.Component {
constructor() {
this.state = {
name: 'Mayank'
};
}

render() {
return (
<>
{this.state.name == 'Mayank' && <AdminHeaderComponent />}
<HeaderComponent />
<ContentComponent />
</>
);
}
}

在上面的代码中,当 name 不是 Mayank 时,React 在位置 1 处放置 null。

11. 为组件创建错误边界

单个的组件错误不应当破坏整个应用, 因此需要创建错误边界来避免在特定组件发生错误时中断.

错误边界涉及一个高阶组件,包含以下方法:static getDerivedStateFromError() 和 componentDidCatch()。

static 函数用于指定回退机制,并从收到的错误中获取组件的新状态。

componentDidCatch 函数用来将错误信息记录到应用中。

import React from 'react';

export class ErrorBoundaries extends React.Component {
constructor(props) {
super(props);
this.state = {
hasErrors: false
};
}

componentDidCatch(error, info) {
console.dir('Component Did Catch Error');
}

static getDerivedStateFromError(error) {
console.dir('Get Derived State From Error');
return {
hasErrors: true
};
}

render() {
if (this.state.hasErrors === true) {
return <div>This is a Error</div>;
}

return (
<div>
<ShowData name="Mayank" />
</div>
);
}
}

export class ShowData extends React.Component {
constructor() {
super();
this.state = {
name: 'Mayank'
};
}

changeData = () => {
this.setState({
name: 'Anshul'
});
};
render() {
if (this.state.name === 'Anshul') {
throw new Error('Sample Error');
}

return (
<div>
<b>This is the Child Component {this.state.name}</b>
<input type="button" onClick={this.changeData} value="Click To Throw Error" />
</div>
);
}
}

如果错误是从 ShowData 函数内抛出的,则它会被父组件捕获,我们使用 static getDerivedStateFromError 函数和 componentDidCatch 生命周期事件中的日志数据部署回退 UI。

12. 组件的不可变数据结构

React 的灵魂是函数式编程。如果我们希望组件能一致工作,则 React 组件中的状态和 props 数据应该是不可变的。

对象的突变可能导致输出不一致。

13. 使用唯一键迭代

React 在列表组件中会添加key值, 给每一个 v-node 添加一个唯一 id, 可以依靠 key 更准确, 更快的拿到 oldnode 中对应的 v-node 节点.

  1. 更准确: 带key的组件会在 diff 的sameNode中进行比较, 避免了就地复用
  2. 更快: 利用key的唯一性生成 map 对象来获取对应节点, 比遍历方式更快.

13.1 不要使用 index 作为 key 值

使用 index 作为键会加大错误率并降低应用的性能。

每当新元素添加到列表时,默认情况下 React 会同时遍历新创建的列表和旧列表,并在需要时进行突变。

在列表顶部添加一个新元素(包含 index 作为键)时,全部已有组件的索引都会更新。

索引更新后,之前键值为 1 的元素现在的键值变成了 2。更新所有组件会拖累性能。

上面的代码允许用户在列表顶部添加新项目。但在顶部插入元素后果最严重。因为顶部元素一变,后面所有的元素都得跟着改键值,从而导致性能下降。

因此,我们应该确保键值和元素一一对应不会变化。

注意:

  • Key 不仅影响性能,更重要的作用是标识。随机分配和更改的值不算是标识。

  • 我们得知道数据的建模方式才能提供合适的键值。如果你没有 ID,我建议使用某种哈希函数生成 ID。

  • 我们在使用数组时已经有了内部键,但它们是数组中的索引。插入新元素时这些键是错误的。

14. React 组件的服务端渲染

服务端渲染可以减少初始页面加载延迟。

我们可以让网页从服务端加载初始页面,而不是在客户端上渲染。这样对 SEO 非常有利。

服务端渲染是指第一个组件显示的内容是从服务器本身发送的,而不是在浏览器级别操作。之后的页面直接从客户端加载。

这样我们就能把初始内容放在服务端渲染,客户端只按需加载部分页面。

15. 谨慎使用 Context

Context 是跨组件传值的一种方案,但我们需要知道,我们无法阻止 Context 触发的 render。

不像 propsstate, React 提供了 API 进行浅比较,避免无用的 render,Context 完全没有任何方案可以避免无用的渲染。

有几点关于 Context 的建议:

  • Context 只放置必要的,关键的,被大多数组件所共享的状态。
  • 对非常昂贵的组件,建议在父级获取 Context 数据,通过 props 传递进来。

16. 小心使用 Redux

Redux 中的一些细节,稍不注意,就会触发无用的 render,或者其它的坑。

精细化依赖

const App = (props)=>{
return (
<div>
{props.project.id}
</div>
)
}
export default connect((state)=>{
layout: state.layout,
project: state.project,
user: state.user
})(App);

在上面的例子中,App 组件显示声明依赖了 redux 的 layout 、 project 、 user 数据,在这三个数据变化时,都会触发 App 重新 render。

但是 App 只需要监听 project.id 的变化,所以精细化依赖可以避免无效的 render,是一种有效的优化手段。

const App = (props)=>{
return (
<div>
{props.projectId}
</div>
)
}
export default connect((state)=>{
projectId: state.project.id,
})(App);

不可变数据

我们经常会不小心直接操作 redux 源数据,导致意料之外的 BUG。

我们知道,JS 中的 数组/对象 是地址引用的。在下面的例子中,我们直接操作数组,并不会改变数据的地址。

const list = ['1'];
const oldList = list;
list.push('a');

list === oldList; //true

在 Redux 中,就经常犯这样的错误。下面的例子,当触发 PUSH 后,直接修改了 state.list ,导致 state.list 的地址并没有变化。

let initState = {
list: ['1']
}

function counterReducer(state, action) {
switch (action.type) {
case 'PUSH':
state.list.push('2');
return {
list: state.list
}
default:
return state;
}
}

如果组件中使用了 ShouldComponentUpdate 或者 React.memo ,浅比较 props.list === nextProps.list ,会阻止组件更新,导致意料之外的 BUG。

所以如果大量使用了 ShouldComponentUpdate 与 React.meo ,则一定要保证依赖数据的不可变性!建议使用 immer.js 来操作复杂数据。

参考链接