使用代码分割技术减少 React 应用首次加载时间

图片来自 Unsplash 的 NordWood Themes
我们使用 React 技术构建大量应用,在我们开发的时候,首要解决的一个问题就是应用的性能问题。当应用变得越来越大,性能可能会呈现断崖式的下降。在所有的性能指标中,首次加载时间受该种情况影响尤为显著。应用在第一次加载的时候应该尽量的快而不是有几秒时间给用户呈现一个白屏,因为加载所需时间越长,给用户留下的印象就越糟糕。
造成上述问题的主要原因是在同一个 bundle 文件中添加了太多组件,所以该 bundle 文件的加载就变得非常耗时。为了避免这种问题,我们需要用一种更优的方式重新组织模块。为了解决这个问题,React 提供了通过代码分割和懒加载使分割后的 bundle 文件更小的方法来解决这个问题。
引入代码分割的最佳位置是路由,基于路由的代码分割将上述问题解决了一大半,但是大多数应用只利用了代码分割技术 50% 的作用。
在使用代码分割技术的时候,我们是否合理地组织了模块结构?接下来,我们将使用一个带有一些 UI 组件的样例 React 应用来探究这个问题是如何发生的以及我们应该如何修复它。
在下文的截图中,我们将看到一个操作面板,该面板含有多个 tab,每个 tab 都含有多个组件。

操作面板使用下面的代码实现基于路由的代码分割。
const Dashboard = lazy(() => import('components/Dashboard'));
const Settings = lazy(() => import('components/Settings'));
const Configurations = lazy(() => import('components/Configurations'));
function App() {
return (
<Router>
<Layout>
<SideBar/>
<Layout>
<AppHeader/>
<Content style={{padding: '20px'}}>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/dashboard">
<Dashboard/>
</Route>
<Route path="/settings">
<Settings/>
</Route>
<Route path="/configuration">
<Configurations/>
</Route>
</Switch>
</Suspense>
</Content>
</Layout>
</Layout>
</Router>
);
}
操作面板包含一些子组件,例如销售额、利润、图表和趋势等,类似下面的代码所实现的。
function Dashboard() {
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Sales" key="1">
<Sales/>
</TabPane>
<TabPane tab="Profit" key="2">
<Profit/>
</TabPane>
<TabPane tab="Chart" key="3">
<Chart/>
</TabPane>
<TabPane tab="Tiles" key="4">
<Tiles/>
</TabPane>
<TabPane tab="Trends" key="5">
<Trends/>
</TabPane>
</Tabs>
);
}
我们已经按照路由分割了代码,所以当应用打包的时候,我们会将不同路由的代码分到不同的代码包中,如下所示。

从上面的图片中可以看出,该文件包含了一个大小为 405.1 KB 的 dashboard 组件以及其他用于构建 Header、sidebar和其他组件和 css 的文件。
我使用 Netlify 托管应用并测试性能。在本地进行测试时我们看不出差别,当我把应用放到 GTmetrix 上时,dashboard 花费了 2.9 seconds 加载。如下图所示

操作面板是该应用的首页,所以当我们通过应用的 URL 访问页面时,405.1KB 大小的文件就会加载,这 405.1KB 大小里包含了首页、侧边栏和头部栏。
一开始,用户只会看到 Sales 选项卡,但是我们的操作面板应用有多个选项卡,浏览器同样会下载其他选项卡的代码,所以这一下载过程就延迟了第一次绘制页面的时间。为了减少首次加载时间,我们需要在操作面板组件的代码中做以下更改:
const Sales = lazy(() => import('components/Sales'));
const Profit = lazy(() => import('components/Profit'));
const Chart = lazy(() => import('components/Chart'));
const Tiles = lazy(() => import('components/Tiles'));
const Trends = lazy(() => import('components/Trends'));
const { TabPane } = Tabs;
function Dashboard() {
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Sales" key="1">
<Suspense fallback={<div>Loading...</div>}>
<Sales/>
</Suspense>
</TabPane>
<TabPane tab="Profit" key="2">
<Suspense fallback={<div>Loading...</div>}>
<Profit/>
</Suspense>
</TabPane>
<TabPane tab="Chart" key="3">
<Suspense fallback={<div>Loading...</div>}>
<Chart/>
</Suspense>
</TabPane>
<TabPane tab="Tiles" key="4">
<Suspense fallback={<div>Loading...</div>}>
<Tiles/>
</Suspense>
</TabPane>
<TabPane tab="Trends" key="5">
<Suspense fallback={<div>Loading...</div>}>
<Trends/>
</Suspense>
</TabPane>
</Tabs>
);
}
在上述代码中我将每个 tab 组件使用懒加载并且用 suspense 组件包裹起来。在这里我用了多个 suspense 组件,但也可以使用一个 suspense 组件包裹所有的组件。
我没有修改路由级别上的代码分割。当我们构建应用的时候,由于对每个选项卡使用了懒加载,因此会有一些额外的文件加入到打包后的文件中,如下图所示:

打包日志
现在我们再次在 GTmetrix 测试应用,这次应用性能如下图所示:

正如你所看到的,我们的操作面板组件在 1 秒之内加载完成,Sales 选项卡刚刚才加载。我们修改的代码为我们减少了将近 2 秒。下图表示了基于路由以及基于路由和组件的代码分割的效果示意图:

基于路由的代码分割

基于路由和组件的代码分割
如图所示,应用的首次加载时间有了很大提高,现在我们通过一些操作面板组件代码中的调整将 React 应用首次加载时间减少了 70%。