概述
当我们进行页面开发时,我们不仅仅是只需要完成页面的功能,同时也要对页面的性能负责。页面的性能是一个比较复杂的话题,当我们讨论页面性能时,首先我们需要知道如何度量页面的性能。比如当我们打开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
包裹,且其实现中拥有 useState
或 useContext
的 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>
);
}
总结
- 性能优化是前端不变的话题,其内容也较为繁多和复杂,本文主要讨论了React项目的代码层面和打包层面的一些优化措施。
- 代码层面主要是通过shouldComponentUpdate、PureComponent和React.memo等方法来避免不必要的渲染和比较,从而提升渲染速度。
- 在代码构建层面,建议使用代码分离的技术来拆分庞大的代码包,React也提供了React.lazy的语法糖以供使用。