大家都知道,随着我们的前端项目不断迭代,其中所包含的代码会越来越多,项目打包后静态资源的体积会不断膨胀。这直接导致了当用户打开网站的时候,页面所需要的加载时间也在不断拉长,进而影响用户体验。特别是当页面内容不分优先级加载,整个页面 loading 不可交互时,用户体验会非常的不好。
让我们来看一个很典型的例子,一个页面由 App 组件构成,而 App 组件中包含了 Header、SideBar、Content 和 Footer 4 个 React 组件
javascript
import React from 'react';
import Header from './Header';
import SideBar from './SideBar';
import Content from './Content';
import Footer from './Footer';
function App() {
return (
<div className="page">
<Header />
<SideBar />
<Content />
<Footer />
</div>
);
}
export default App;

(蓝色模块均为首屏加载组件)
进入页面后,我们在开发者工具 Network 里可以看到,在网络为 Fast 3G 环境下,App 组件的打包文件 main.chunk.js,大小为 7.6kB,加载时间为 774ms。

让我们思考这样一个问题,有没有可能在保证页面功能完整的前提下,最大程度压缩页面加载 App 组件的时间,从而提升初始化页面的可交互时间?我们可以通过延迟加载 APP 组件中一些非关键的子组件,从而减小 App 组件的体积,以便能够更快速的打开页面。
你听到这里可能已经有一个大致的思路了,接下来让我们先来了解下如何在 React 中执行。这就要用到的 React.lazy 函数,它可以让我们像渲染常规组件一样处理动态引入(的组件)。我们正常引用一个组件,可以采用 ESM 的 import 语法
javascript
import OtherComponent from './OtherComponent';
如果希望能够延迟加载 OtherComponent 组件,我们可以使用 React.lazy 来加载这个组件
ini
const OtherComponent = React.lazy(() => import('./OtherComponent'));
React.lazy 的参数是一个回调函数,在这个回调函数中调用 了 import 方法,加载 OtherComponent 组件。import 执行后会返回一个 Promise,当这个 Promise resolve 时会返回 一个真正的 React 组件,即 OtherComponent。
这样,引入的组件就只会在首次真正被渲染时才会被加载,而在此之前并不会产生额外的相关请求,这就达到了延迟加载的效果。
前面给大家讲解了 React.lazy 的用法,接下来我们讲讲 Suspense 组件,Suspense 组件是配合 React.lazy 一起使用的。
Suspense 组件的使用非常简单,只需要用它包裹 React.lazy 引入的组件就可以了。并且 Suspense 还为开发者提供了一些降级的手段,设置 fallback 为 Loading...,这样可以在等待加载 lazy 组件时提示用户内容加载中。
javascript
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
React.lazy 和 Suspense 组件的用法已经告诉大家了,现在我们来修改之前的例子,为 App 组件添加组件延迟加载的功能。
按照通常情况来说,当用户最开始进入页面的时候,首先看到的一定是 Header 组件和 SideBar 组件,之后才会关注到内容区域,也就是 Content 组件呈现的部分,最后才是页脚所对应的 Footer 组件,这就意味着 Content 组件和 Footer 组件并不是页面加载时候的关键组件,它们可以被延迟加载。
javascript
import React, { Suspense } from 'react';
import Header from './Header';
import SideBar from './SideBar';
// content和footer部分延迟加载
const Content = React.lazy(() => import('./Content'));
const Footer = React.lazy(() => import('./Footer'));
function App() {
return (
<div className="page">
<Header />
<SideBar />
<Suspense fallback={<div>Loading...</div>}>
<Content />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<Footer />
</Suspense>
</div>
);
}
export default App;

(蓝色模块为首屏加载,黄色模块延迟加载)
我们观察到 App 组件打包文件 (main.chunk.js) 大小为 4.5kB,耗时 664ms,也就是在延迟加载了 Content、Footer 组件后,相比之前的 7.6kB,所需的资源减少了 3.1kB,即减少了 41% 的体积,耗时减少了 110ms,即加快了 14%。
可以看得出,使用组件延迟加载后,首屏加载的资源体积有了明显的减少,同样网络条件下,首屏资源加载速度也会有一定比例的提升。我们上面的例子很简单,代码原始体积并不是很大,但大家可以想想,对于一个大型复杂的业务应用来说,特别是页面组件多,组件展现优先级分明的页面,优先加载主要组件,延迟加载其他组件,会非常直接的提前用户可交互时间点,提升用户体验。

延迟加载除了在一个页面引入多组件的场景外,另一个典型场景,就是可以在路由入口中分割代码。
接下来让我们来看看通过路由如何延迟加载组件:
我们设置一个页面,有 3 个菜单即 3 个路由入口,分别为/home 路由对应 Home 组件,/business 路由对应 Business 组件,/manage 路由对应 Management 组件:
javascript
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import Home from './Home';
import Business from './Business';
import Management from './Management';
export default function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/business">Business</Link>
</li>
<li>
<Link to="/manage">Management</Link>
</li>
</ul>
<hr />
<Switch>
<Route exact path="/home">
<Home />
</Route>
<Route path="/business">
<Business />
</Route>
<Route path="/manage">
<Management />
</Route>
</Switch>
</div>
</Router>
);
}
打开浏览器,访问 Home 页面:/home
我们可以看到,在开发者工具的 network 面板设置 Fast 3G 网络下,App 组件打包文件 (mian.chunk.js) 大小为 6.8kB,包含了 Home, Business, Management 等三个路由下的组件,共加载了 785ms。
加载完成且页面渲染完成后,菜单路由之间切换,无加载延迟。
让我们想象这样一个场景,用户每次进入系统,默认会先进入 Home 页面 (此时 Home 页面为首屏页面) 进行浏览和操作,之后再进入其他页面使用,甚至有可能用户只会在 Home 页面停留,其他两个页面并不会访问。如果我们想要加快访问 Home 页时的速度,又不影响访问系统的全局功能,其实可以通过延迟加载其他路由内容,减少加载 Home 页时的资源,来做一些优化。
我们保持 Home 组件的引入方式,对 Business 和 Management 组件进行延迟加载,仅在访问/business 和/manage 路由时,才分别加载这两个组件:
javascript
import React, { Suspense } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import Home from './Home';
// 延迟加载路由下的Business和Management组件
const Business = React.lazy(() => import('./Business'));
const Management = React.lazy(() => import('./Management'));
export default function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/business">Business</Link>
</li>
<li>
<Link to="/manage">Management</Link>
</li>
</ul>
<hr />
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/home">
<Home />
</Route>
<Route path="/business">
<Business />
</Route>
<Route path="/manage">
<Management />
</Route>
</Switch>
</Suspense>
</div>
</Router>
);
}
访问 Home 页面。
我们查看 Network 的资源请求,访问首屏 Home 页时加载的 main.chunk.js 体积为为 5.2kB,耗时 703ms,相比于之前组件全部加载时的 6.8kB 减少了 1.6kB,相当于减少了 23% 的体积,耗时减少 82ms,相当于缩短了 10% 的时间。
并且从视频中我们可以看到,当我们点击 Business 菜单后,页面出现 Suspense 设置的 fallback 内容:loading...,且在 Network 中可以看到新加载的 1.chunk.js 文件,再点击 Management 菜单,也可以看到新加载的 3.chunk.js 文件。
回看我们的代码:
xml
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/home">
<Home />
</Route>
<Route path="/business">
<Business />
</Route>
<Route path="/manage">
<Management />
</Route>
</Switch>
</Suspense>
因为 Business 和 Management 组件包裹在同一个 Suspense 内,所以共享了相同的 fallback:loading...,这样既避免了为每个组件都设置 loading 状态,也便于全局路由入口 loading 的统一管理。
另外,一般情况下我们的系统不仅会有很多路由入口,且会有不同的角色,比如 A 和 B,不同的角色又会访问不同的页面。假设 A 只可以访问 Home 和 Business 页面,B 只可以访问 Home 和 Management 页面。那么我们在对 Home 和 Business 组件延迟加载后,不仅提高了两个角色访问 Home 页面的速度,而且 A 无需访问的 Management 页面资源也不用浪费请求进行加载了,B 对于 Business 页面亦是。这样的页面访问速度优化是立竿见影的,也是相对容易实现的。
最后我们来总结下今天的内容:当我们的项目随着迭代代码不断增长,打包文件体积逐渐变大时,用户进入页面的加载时间也越来越长,有时候用户甚至会加载一些不需要的代码资源,这些都拉长了资源的请求时间,推迟了用户与页面的可交互时间点,用户体验不是很好。这个时候我们可以利用 code-splitting 这项技术分割代码,来对这些问题进行一些优化。
而在 React 中,React.lazy 函数配合 Suspense 组件就可以帮助我们实现 code-splitting,延迟加载组件。具体的实现是通过 React.lazy 接收的函数中调用 import 来引入组件 (const OtherComponent = React.lazy(() => import('./OtherComponent'))),并在使用的时候用 Suspense 组件包裹引入的组件,通过设置 Suspense 组件的 fallback 属性来展示组件延迟加载时的 loading 状态。
这样使用 lazy+ Suspense 延迟加载组件后,组件只有需要渲染时,页面才会加载组件资源,我们不仅可以在单个页面中使用这个方法,还可以在项目路由的入口处使用来延迟加载用户未使用的路由资源,避免无用资源的请求,进而提高页面首屏资源的加载速度,提升用户体验。
