使用 React.lazy 和 Suspense 进行代码拆分

您永远不需要向用户交付超出必要的代码,因此请拆分您的 bundles 以确保这种情况永远不会发生!

通过 React.lazy 方法,可以使用动态导入在组件级别轻松地对 React 应用程序进行代码拆分。

import React, { lazy } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const DetailsComponent = () => (
  <div>
    <AvatarComponent />
  </div>
)

为什么这很有用?

大型 React 应用程序通常由许多组件、实用程序方法和第三方库组成。如果没有努力尝试仅在需要时才加载应用程序的不同部分,则会在用户加载第一个页面后立即向他们交付一个大型 JavaScript bundle。这可能会严重影响页面性能。

React.lazy 函数提供了一种内置方法,可以将应用程序中的组件分隔成单独的 JavaScript 代码块,而无需进行太多准备工作。然后,当您将其与 Suspense 组件结合使用时,可以处理加载状态。

Suspense

向用户交付大型 JavaScript 有效负载的问题在于页面完成加载所需的时间,尤其是在性能较弱的设备和网络连接上。这就是代码拆分和延迟加载非常有用的原因。

但是,当通过网络获取代码拆分的组件时,用户总是会遇到轻微的延迟,因此显示有用的加载状态非常重要。将 React.lazySuspense 组件结合使用有助于解决此问题。

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
  </Suspense>
)

Suspense 接受一个 fallback 组件,该组件允许您显示任何 React 组件作为加载状态。以下示例展示了其工作原理。仅当单击按钮时才会渲染头像,此时会发出请求以检索 suspended AvatarComponent 所需的代码。同时,会显示 fallback 加载组件。

在这里,构成 AvatarComponent 的代码很小,这就是加载微标仅显示很短时间的原因。较大的组件可能需要更长的时间才能加载,尤其是在弱网络连接上。

为了更好地演示其工作原理

  • 要预览网站,请按查看应用。然后按全屏 全屏
  • 按 `Control+Shift+J`(在 Mac 上按 `Command+Option+J`)打开 DevTools。
  • 点击 Network 选项卡。
  • 点击Throttling 下拉菜单,默认设置为 No throttling。选择 Fast 3G
  • 点击应用中的 Click Me 按钮。

现在加载指示器将显示更长时间。请注意,构成 AvatarComponent 的所有代码是如何作为单独的代码块获取的。

DevTools network panel showing one chunk.js file being downloaded

暂停多个组件

Suspense 的另一个功能是,它允许您暂停加载多个组件,即使它们都是延迟加载的

例如

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
    <InfoComponent />
    <MoreInfoComponent />
  </Suspense>
)

这是一种非常有用的方法,可以在仅显示单个加载状态的同时延迟多个组件的渲染。一旦所有组件都完成获取,用户就可以同时看到它们全部显示出来。

您可以通过以下嵌入内容看到这一点

如果没有这个,很容易遇到交错加载问题,或者 UI 的不同部分一个接一个地加载,每个部分都有自己的加载指示器。这会使用户体验感觉更加突兀。

处理加载失败

Suspense 允许您在后台进行网络请求时显示临时加载状态。但是,如果这些网络请求由于某种原因而失败怎么办?您可能处于离线状态,或者您的 Web 应用可能正在尝试延迟加载过期的版本化 URL,并且在服务器重新部署后不再可用。

React 有一种标准模式可以优雅地处理这些类型的加载失败:使用错误边界。正如文档中所述,如果任何 React 组件实现了生命周期方法 static getDerivedStateFromError()componentDidCatch() 中的任一方法(或两者都实现),则它可以充当错误边界。

要检测和处理延迟加载失败,您可以使用充当错误边界的父组件来包装您的 Suspense 组件。在错误边界的 render() 方法内部,如果没有错误,您可以按原样渲染子组件,或者在出现问题时渲染自定义错误消息

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error) {
    return {hasError: true};
  }

  render() {
    if (this.state.hasError) {
      return <p>Loading failed! Please reload.</p>;
    }

    return this.props.children;
  }
}

const DetailsComponent = () => (
  <ErrorBoundary>
    <Suspense fallback={renderLoader()}>
      <AvatarComponent />
      <InfoComponent />
      <MoreInfoComponent />
    </Suspense>
  </ErrorBoundary>
)

结论

如果您不确定从哪里开始将代码拆分应用于您的 React 应用程序,请按照以下步骤操作

  1. 从路由级别开始。路由是识别应用程序中可以拆分的点的最简单方法。React 文档展示了如何将 Suspensereact-router 一起使用。
  2. 识别您网站页面上仅在某些用户交互(例如单击按钮)时才渲染的任何大型组件。拆分这些组件将最大限度地减少您的 JavaScript 有效负载。
  3. 考虑拆分任何屏幕外且对用户不重要的其他内容。