React.lazy初体验
React.lazy项目实践
React16.6于2018年10月份发布,该版本带来了许多新的特性同时赋予给React更强大的功能。其中最为显著的两个特性是React.Suspense和React.lazy。这两个特性,将React的代码分割和懒加载带到了一个新的高度。使用这两个特性,你可以做到的是在真正需要时才加载该组件的文件。
本文主要介绍我在项目中如何使用React.Suspense和React.lazy以及该特性给我们React开发者带来的好处。
为什么要使用代码分割
随着前端技术的不断发展,ES6模块、Babel转换、webpack打包等新技术的出现,前端应用现在完全可以使用模块化的方式完成,便于维护。
通常情况下,我们会将所有的模块打包到一个文件中,当请求网页时加载该文件以展示整个应用。但是,随着网页功能的不断扩展,这便带来了网页加载缓慢、交互卡顿等问题,使用户体验非常糟糕。
导致这一问题的主要原因是,我们在页面加载时,会一次性加载所有代码,无论是当前要用的代码还是之后用到的代码。但是用户在第一次进来时并不会用到所有的功能,因此code-splitting即代码分割这个名词出现了。
像webpack便提供了代码分割的功能。webpack中对代码分割的定义如下:
Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel.
意思就是说可以将代码拆分为多个bundle,同时可以按需或者并行加载。因此,为了提高应用的性能,我们可以尝试如何合理的分割代码并延迟加载。
如何分割代码?
动态加载
ES6标准引入了import以方便我们静态加载模块。形式如:1
import xxx from xxx.js
尽管import对于我们加载模块很有帮助,但是静态加载模块的方式一定程度上限制了我们来实现异步模块加载。不过,目前动态加载模块的import()语法已处于提案阶段,并且webpack已将他引入并使用。import()提供了基于Promise的API,因此,import()的返回值是一个完成状态或拒绝状态的Promise对象。形式如:1
2
3
4
5import(/* webpackChunkName: 'module'*/ "module")
.then(() => {
//todo
})
.catch(_ => console.log('It is an error'))
webpack在编译时,识别到动态加载的import语法,则webpack会为当前动态加载的模块创建一个单独的bundle。如果你使用的是官方的Create-react-app脚手架或React的服务端渲染框架Next.js,那么可以直接使用动态import语法。如果你的脚手架是你自己配置的webpack,那么你需要按照官方指南来设置,请移步[1]。
动态加载React组件
当前最为流行的一种方法是使用React-loadable[2]库提供的懒加载React组件。它利用import()语法,使用Promise语法加载React组件。同时,React-loadable支持React的服务端渲染。
通常,我们以如下方式实现组件:1
2
3
4
5
6
7
8
9
10import LazyComponet from 'LazyComponent';
export default function DemoComponent() {
return (
<div>
<p>demo component</p>
<AComponent />
</div>
)
}
在上面的例子中,假设LazyComponet在DemoComponent渲染时我们并不展示。但是因为我们使用import语法将LazyComponet导入,所以在编译时会将LazyComponet的代码与DemoComponent的代码打包到同一个bundle里面。
但是,这并不是我们想要的。所以我们可以通过使用React-loadable来懒加载LazyComponet,同时将LazyComponet的代码单独打包到一个bundle里面。我们可以看一下官网提供的例子:1
2
3
4
5
6
7
8
9
10
11
12
13import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
从例子中我们可以看到,react-loadable使用动态import()方法,并将导入的组件分配给loader属性。同时,react-loadable提供了一个loading属性,以设置在加载组件时将展示的组件。
React.lazy和React.suspense的使用
React.lazy and Suspense is not yet available for server-side rendering. If you want to do code-splitting in a server rendered app, we recommend Loadable Components. It has a nice guide for bundle splitting with server-side rendering.
在使用之前,我们需要特别注意的一点是,React官方明确支持,React.lazy和Suspense并不支持服务端渲染。因此,使用服务端渲染的同学,请绕行至react-loadable和loadable-components[3]。
由于我是对原有项目进行的升级,因此,本文以下内容主要针对于老项目升级React最新版所做的工作。
代码升级React最新版
如果你的代码是Reactv16,那么可以直接升级到最新版本,当然React16.6已经提供了lazy和suspense方法。如果是v16之前,则按照官方操作迁移。
确定原有代码的懒加载组件
首先,按照需求,将非首屏加载的组件确定为懒加载组件,我的项目共确定五个组件可以进行懒加载。修改方式很简单,原有引入组件的方法为:1
import LazyComponent from "../components/LazyComponent ";
修改为:1
2
3const LazyComponent = React.lazy(() =>
import(/* webpackChunkName: 'lazyComponent'*/ "../components/LazyComponent")
);
如代码所示:将静态引用组件的代码替换为调用React.lazy(),在lazy()传入一个匿名函数作为参数,在函数中动态引入lazyComponent组件。这样在我们渲染这个组件前,浏览器将不会下载lazyComponent .bundle.js文件和它的依赖。
其中,import内的webpackChunkName为我们定义的bundle文件名。
如果React要渲染<React.Suspense/>的出现帮我们解决问题。在代码未下载好前,它将会渲染fallback属性传入的值。因此我们的原有代码为:1
2
3
4
5
6return (
<div>
<MainComponet />
<LazyComponent />
</div>
)
修改为:1
2
3
4
5
6
7
8return (
<div>
<MainComponet />
<React.Suspense fallback={<div>正在加载中</div>}>
<LazyComponent />
</React.Suspense>
</div>
)
fallback中可以修改为任意的spinner,本次不做过多优化。假如你不使用React.suspense,则React会给出你错误提示,因此记得React.lazy和React.Suspense搭配使用。
此时我们可以看一下我们的网络请求。

从图中我们可以看到,我们动态加载的lazyComponet组件被单独打包到一个bundle里面,然而,在首屏加载的时候,该bundle已经加载到我们的页面中了,这也许并不是我们想要的,我们想要的是当我们需要的时候再加载。接下来我们就控制一下,当我们需要的时候,再加载该文件。
通过变量控制加载
原本我选择的五个懒加载组件均属于弹层性质的组件,因此必然会设置一个state来控制该组件的显示与隐藏,因此我们将代码改为:
1 | return ( |
此时我们可以看一下网络请求。

从图中可以看出,在首屏加载时,并未加载我们的懒加载组件LazyComponent所对应的bundle包。等到我们点击需要该组件显示时,页面才去加载该js。这便达到了我们代码分离并懒加载的目的。那么我们这么操作,到底主bundle包的体积减少了吗?接下来我们打包文件看一下。
打包文件
拆分前打包出来的文件:

拆分后打包出来的文件:

app.js文件变小,随之增加lazyComponent.js。当懒加载组件多时,我们便可一定程度上减少首屏加载文件的大小,提高首屏的渲染速度。
浏览器参数对比
DOMContentLoaded
拆分前:

拆分后:

performance summary
拆分前:

拆分后:

- 蓝色:加载(Loading)时间降低
- 黄色:脚本运算(Scripting)时间降低
- 紫色:渲染(Rendering)时间降低
- 绿色:绘制(Painting)时间持平
- 灰色:其他(Other)时间降低
- 闲置:浏览器空闲时间降低
接口请求时间
拆分前:


拆分后:


从图中可以看出,拆分后接口请求数据时间提前。
总结
从多项数据表明,React.lazy和React.Suspense的使用一定程度上加快了首屏的渲染速度,使得我们的页面加载更快。
另外,当我们想添加一个新功能而引入一个新依赖时,我们往往会评估该依赖的大小以及引入该依赖会对原有bundle造成多大影响。假如该功能很少被用到,那么我们可以痛快地使用React.lazy和React.Suspense来按需加载该功能,而无需牺牲用户体验了。
扩展阅读
[1] https://webpack.js.org/guides/code-splitting/
[2] https://github.com/jamiebuilds/react-loadable
[3] https://github.com/smooth-code/loadable-components