手把手教你React(六)- 性能优化

1,258 阅读6分钟

概述

当我们进行页面开发时,我们不仅仅是只需要完成页面的功能,同时也要对页面的性能负责。页面的性能是一个比较复杂的话题,当我们讨论页面性能时,首先我们需要知道如何度量页面的性能。比如当我们打开Chrome浏览器开发者模式中的lightHouse工具去给页面打分的适合,会出现诸如First Contentful Paint、Time to Interactive等指标,这些可以看作是对页面表现的一个评分。

当我们对我们的网站表现有了一个评估之后,下一步就是针对这个评分,对于页面渲染的各个阶段进行针对性的优化,当我们打开一个网站的时候,我们经历了DNS解析、浏览器请求页面、页面各个静态资源的请求,最后是渲染。其实当我们在做优化是主要也是从请求和渲染这两个方向进行优化。

关于前端性能的优化我们很难在一篇文章中阐述清楚,所以这篇文章并不会讨论性能的度量以及请求的优化,这些我们会在下一个专门介绍性能优化的系列中进行介绍,而本文主要讨论React开发中的关于代码打包和页面渲染两个方面的具体的优化策略。

React项目代码层面的性能优化

当我们提到React时,我们总会说React提升了页面的渲染性能,因为操作DOM大代价是高昂的,而操作js是成本低廉的,React提出了包括虚拟DOM和diff算法在内的一系列优化。所以对于大多数情况来说,当我们使用React时我们已经有了较高性能的页面,但是React仍提供了一些口子,以供我们进行性能上的优化。

使用生产版本

当你需要对你的 React 遇到了性能问题,请确保你正在使用压缩后的生产版本。React 默认包含了许多有用的警告信息。这些警告信息在开发过程中非常有帮助。然而这使得 React 变得更大且更慢,所以你需要确保部署时使用了生产版本。

同时,我们需要确认我们使用的是压缩后的React代码,使用带min.js的js文件

<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

或者使用webpack等构件工具对React代码进行压缩。

shouldComponentUpdate 

当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。

即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 true,让 React 执行更新:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代表返回的 React 元素是否相同。显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。

PureComponent

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。举个例子:

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

只有当this.props.color或者this.state.count变化时该组件才会更新。当使用PureComponent时我们需要注意,该组件只会进行浅比较,当我们使用数组或者对象时,尽管你可能修改了该数组或者对象中的某个值,但PureComponent只会比较两个数组,但由于数组并未改变所以组件不会更新。

最简单的解决该问题的方法就是使用concat和assign来处理:

this.setState(state => ({
  words: state.words.concat(['marklar'])
}));

this.setState(state => ({
  colormap: Object.assign({}, colormap, {right: 'blue'})}));

React.Memo

当我们使用函数式组件时,我们可以使用React.memo来代替PureComponent

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

React.memo 为高阶组件。

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

代码分离

随着前端工程化技术的发展,代码打包的技术也在不断增强,但是随着项目越来越复杂,但随着你的应用增长,你的代码包也将随之增长。尤其是在整合了体积巨大的第三方库的情况下。你需要关注你代码包中所包含的代码,以避免因体积过大而导致加载时间过长。

动态import

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ES 的 import语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure

import("./math").then(math => {
  console.log(math.add(16, 26));
});

当 Webpack 解析到该语法时,会自动进行代码分割。更多关于代码分离的内容可以参考这里

React.lazy

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

总结

  1. 性能优化是前端不变的话题,其内容也较为繁多和复杂,本文主要讨论了React项目的代码层面和打包层面的一些优化措施。
  2. 代码层面主要是通过shouldComponentUpdate、PureComponent和React.memo等方法来避免不必要的渲染和比较,从而提升渲染速度。
  3. 在代码构建层面,建议使用代码分离的技术来拆分庞大的代码包,React也提供了React.lazy的语法糖以供使用。