Kodokon kodokon.com

Suspense、lazy 与代码分割

掌握挂起机制、组件的懒加载以及打包产物的分割。

9 分钟 · 3 题

在 Kodokon 中打开本课

Suspense 依赖一个鲜为人知的机制:一个尚未拥有其数据或代码的组件会挂起它的渲染。在底层,当前的实现会抛出一个 Promise,React 会像拦截错误那样拦截它。React 随后会向上寻找最近的 <Suspense> 边界,显示它的 fallback,然后在 Promise 解决后重放该子树的渲染。这是一个实现细节(永远不要自己抛出 Promise),但它解释了所有可观察到的行为:这个边界就像针对异步工作的一个声明式 catch。历史上的典型用例是使用 React.lazy 进行代码分割

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

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

function Dashboard() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart points={data} />
    </Suspense>
  );
}
Chart 的代码只在它首次渲染时才被下载。

lazy 期望一个返回 Promise 的函数,该 Promise 解决为一个带有 default 导出的模块。分割本身是打包器的工作:每个动态 import() 都会成为一个分割点并生成一个独立的 chunk,按需加载。在真实代码库中一个常见的细节:你的组件往往是具名导出。这时你需要即时地对模块进行改造。

JAVASCRIPT
const Chart = lazy(() =>
  import('./charts').then((module) => ({
    default: module.Chart,
  }))
);
把具名导出适配到 React.lazy 的约定。

生产环境中棘手的部分:在一次导航期间,已经显示的内容可能会再次挂起(新的路由、新的 chunk)并被 fallback 粗暴地替换。自 React 18 起,用 startTransition 包裹的更新改变了这一行为:React 会在新渲染于后台挂起时把之前的界面保留在屏幕上,只对从未展示过的内容显示 fallback。边界还能在深度上组合:多个嵌套的 <Suspense> 边界让你能一块区域接一块区域地揭示界面,从全局骨架一直细化到细节。

JSX
const [isPending, startTransition] = useTransition();

function openTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}
// The current tab stays visible while the new
// one loads, instead of dropping back to the fallback.
过渡能避免在导航期间退回到 fallback。

知识检测

确认你已牢记本课的重点内容。

  1. 传给 React.lazy 的函数必须返回什么?
    • 直接返回一个 React 组件
    • 一个解决为带 default 导出的模块的 Promise
    • 一个对象 { component, loading }
    • 一个包含文件路径的字符串
  2. 如何防止已显示的内容在导航挂起时被 fallback 替换?
    • 把状态更新包裹在 startTransition 中
    • 给 Suspense 边界传入 fallback={null}
    • 使用 useEffect 来延迟导航
    • 移除父级的 Suspense 边界
  3. 究竟是谁把代码分割成独立的 chunk?
    • React,在提交阶段
    • 浏览器,通过 HTTP 缓存
    • 打包器,从每个动态 import() 出发