React 19.x 的 lazy 与 Suspense
React.lazy
在构建大型 React 应用时,打包体积过大往往会影响首屏加载速度。React.lazy 正是为了解决这一问题而诞生的内置函数,它让你可以将组件动态导入(code splitting),并按需加载,从而显著提升应用性能。
为何使用 React.lazy?
- 减少初始包体积:应用的首屏可能不需要所有组件。通过代码分割,只加载当前路由或交互所需的组件。
- 提升首屏加载速度:更少的 JavaScript 意味着更快的解析和执行时间,改善用户体验。
- 优化缓存与带宽:用户可能只使用部分功能,懒加载未使用的代码可节省流量。
- 与 Suspense 天然集成:React.lazy 配合 Suspense 可以优雅地显示加载状态(如 Loading 动画)。
渲染阶段流程
React.lazy 组件加载完成的触发机制依赖于 Promise 的 resolve 回调 和 React 内部的 Suspense 重试(ping)机制。
一、 初始化
调用 React.lazy(() => import('./Component')) 会生成一个特殊的“懒加载对象”,内部包含 _status(初始为 Uninitialized)和 _result(存储加载器函数)。
二、 首次渲染
当 React 遇到这个懒加载对象时,会调用内部函数 lazyInitializer:
- 执行
_result()(即import()),得到一个 Promise(thenable)。 - 将
_status更新为Pending,_result指向该 Promise。 -
抛出该 Promise 以触发最近的
<Suspense>边界.
三、 suspense 捕获与监听
React 捕获抛出的 Promise,向上查找 Suspense 边界:
- 调用
attachPingListener为该 Promise 添加一个then回调(即ping函数)。 - 该边界立即渲染
fallbackUI。
四、 加载完成(Promise resolve)
当动态导入的模块成功加载后,Promise 被 resolve,模块对象作为结果返回。Promise 的回调执行:
- 将 lazy 对象的
_status更新为Resolved,_result替换为模块对象。 - 调用之前附加的
ping回调(实际上是pingSuspendedRoot)。
五、 触发重新渲染
pingSuspendedRoot 会标记对应根节点的优先级车道(pingedLanes),并调用 ensureRootIsScheduled,重新调度整个应用的渲染(或仅重试该 Suspense 边界)。
六、 二次渲染
React 再次执行该组件的渲染逻辑,此时 lazyInitializer 发现 _status === Resolved,直接返回 _result.default(真正的组件),从而正常完成渲染,替换 fallback。
注意事项
-
避免在渲染函数内动态调用
lazy:lazy应在模块顶层定义,确保每次渲染都得到相同的引用。 - 重复导入优化:同一个 lazy 组件在多处使用时,内部会共享相同的 Promise,不会重复加载。
- 错误处理:懒加载可能因网络问题失败,建议结合错误边界(Error Boundary)捕获加载失败错误。
-
命名导出问题:默认导出是
lazy的约定,非默认导出需手动转换。 - 避免在 Suspense 外部调用 lazy 组件:否则无法捕获挂起。
示例 懒加载组件
lazy 接收一个函数,该函数必须返回一个动态 import() 调用(返回 Promise,其 resolve 值为包含 React 组件的模块)。Suspense 用于包裹懒加载组件,并在等待期间渲染 fallback 内容。
import { lazy, Suspense, useState } from "react";
const Card = lazy(() => import("./Card"));
const SuspenseB = () => {
const [num, setNum] = useState(0);
return (
<div className="suspense-b">
<p>num: {num}</p>
<button onClick={() => setNum(num + 1)}>click</button>
<Suspense fallback={<div className="suspense-b-fallback">Loading...</div>}>
<Card />
</Suspense>
</div>
);
};
export default SuspenseB;
懒加载组件开始到成功的 三个阶段
初始化 状态 -1
![]()
加载中 状态 0
![]()
加载完成 状态 1
![]()
import { useState, } from "react";
const Card = () => {
const [count, setCount] = useState(0);
return (
<div className="card">
<p>Count: {count}</p>
<button onClick={() => setCount((val) => val + 1)}>Click me</button>
</div>
);
};
export default Card;
调用 React.lazy(() => import('./Component')) 会生成一个特殊的“懒加载对象”,内部包含 _status(初始为 Uninitialized)和 _result(存储加载器函数)。
![]()
![]()
beginWork
![]()
fiber.elementType
![]()
resolveLazy 解析 lazy 组件
![]()
lazy._init(lazy._payload) 执行初始化函数
![]()
回到 resolveLazy 解析 lazy 组件
全局变量 suspendedThenable 为 promise pending状态
![]()
const SuspenseException: mixed = new Error(
"Suspense Exception: This is not a real error! It's an implementation " +
'detail of `use` to interrupt the current render. You must either ' +
'rethrow it immediately, or move the `use` call outside of the ' +
'`try/catch` block. Capturing without rethrowing will lead to ' +
'unexpected behavior.\n\n' +
'To handle async errors, wrap your component in an error boundary, or ' +
"call the promise's `.catch` method and pass the result to `use`.",
);
handleThrow 负责处理渲染过程中抛出的各种异常(包括普通错误和 Suspense 挂起)
![]()
getSuspendedThenable
全局变量重置 为 null ,返回 promise pending
![]()
回到 handleThrow
![]()
renderRootSync
全局变量 workInProgressSuspendedReason 为 3, 代表 SuspendedOnImmediate 因任务立即挂起
![]()
找到边界
![]()
// 未挂起,正常渲染
const NotSuspended: SuspendedReason = 0;
// 渲染过程中抛出异常
const SuspendedOnError: SuspendedReason = 1;
// 等待异步数据
const SuspendedOnData: SuspendedReason = 2;
// 因立即任务挂起
const SuspendedOnImmediate: SuspendedReason = 3;
// 因实例挂起
const SuspendedOnInstance: SuspendedReason = 4;
// 因实例挂起但准备继续
const SuspendedOnInstanceAndReadyToContinue: SuspendedReason = 5;
// 因废弃的 Promise 挂起
const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 6;
// 挂起准备继续
const SuspendedAndReadyToContinue: SuspendedReason = 7;
// 因 hydration 挂起
const SuspendedOnHydration: SuspendedReason = 8;
// 因 action 挂起
const SuspendedOnAction: SuspendedReason = 9;
throwAndUnwindWorkLoop 处理渲染过程中的异常/挂起,展开栈并找到处理边界
![]()
throwException 处理渲染阶段的异常和 Suspense
![]()
attachPingListener 为 Suspense 边界的挂起 Promise(wakeable)添加“ping”监听器
![]()
渲染 fallback
再次进入 ,lazy组件还是加载中
![]()
懒加载组件加载完成
![]()
加载完毕是一个函数组件
![]()
示例 命名导出组件的懒加载
const InfoCard = lazy(() =>
import("./Card").then((mod) => ({ default: mod.InfoCard })),
);
export const InfoCard = () => {
const [count, setCount] = useState(0);
return (
<div className="info-card">
<p>Info Card</p>
<button onClick={() => setCount((val) => val + 1)}>
InfoCard-Click me
</button>
<p>InfoCard Count: {count}</p>
</div>
);
};
beginWork 阶段
fiber.tag = 16 , 代表 LazyComponent
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
![]()
completeWork 阶段
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
源码
const Uninitialized = -1; // 未初始化,未调用
const Pending = 0; // 加载中
const Resolved = 1; // 加载成功
const Rejected = 2; // 加载失败
function lazy<T>(
ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
// 创建 payload 对象
const payload: Payload<T> = {
// We use these fields to store the result.
_status: Uninitialized, // 初始化,未调用
_result: ctor, // 存储工厂函数
};
// 创建 lazyType 对象
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE, // 标识是 lazy 组件
_payload: payload, // 存储 payload 对象,加载信息
_init: lazyInitializer, // 初始化函数
};
return lazyType;
}
- 未初始化状态(
_status === Uninitialized),状态变为Pending,执行throw payload._result;,抛出 thenable。这正是 React Suspense 的触发点。 -
成功回调:当模块加载成功时,将
payload._status设置为Resolved,payload._result设置为模块对象。 -
失败回调:将
payload._status设置为Rejected,payload._result设置为错误对象。如果是Rejected,抛出错误,由最近的错误边界(Error Boundary)捕获
function lazyInitializer<T>(payload: Payload<T>): T {
// 未初始化处理
if (payload._status === Uninitialized) {
let resolveDebugValue: (void | T) => void = (null: any);
let rejectDebugValue: mixed => void = (null: any);
const ctor = payload._result; // 加载器函数 () => import("")
const thenable = ctor(); // 加载器函数返回的 Thenable 对象
// 监听 Promise 状态变化
thenable.then(
moduleObject => { // 加载成功
// 正在加载、未初始化
if (
(payload: Payload<T>)._status === Pending ||
payload._status === Uninitialized
) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved; // 设置状态为加载成功
resolved._result = moduleObject; // 设置结果为模块对象
if (thenable.status === undefined) {
const fulfilledThenable: FulfilledThenable<{default: T, ...}> =
(thenable: any);
fulfilledThenable.status = 'fulfilled'; // 设置状态为加载成功
fulfilledThenable.value = moduleObject; // 设置值为模块对象
}
}
},
// 加载失败
error => {
if (
(payload: Payload<T>)._status === Pending ||
payload._status === Uninitialized
) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected; // 设置状态为加载失败
rejected._result = error; // 设置结果为错误对象
if (thenable.status === undefined) {
const rejectedThenable: RejectedThenable<{default: T, ...}> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
}
},
);
// 未初始化
if (payload._status === Uninitialized) {
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
}
}
// 加载成功
if (payload._status === Resolved) {
const moduleObject = payload._result;
return moduleObject.default; // 返回模块对象的默认导出
} else {
// 抛出 thenable。这正是 React Suspense 的触发点
throw payload._result;
}
}
Suspense
Suspense 是 React 内置的组件,用于包裹那些可能“挂起”(Suspend)的子组件。当子组件抛出 Promise(或 React 内部的 Suspense 异常)时,Suspense 会捕获并渲染 fallback 属性指定的占位内容,直到 Promise 解决后重新渲染子组件。
suspense 能够实现:
- 并行等待多个资源。
- 避免加载闪烁(快速加载时不显示 fallback)。
- 与错误边界(Error Boundary)无缝集成。
注意事项
- 避免在
fallback中再使用 Suspense - Suspense 不能捕获错误 。它只处理 Promise 挂起,普通错误(如运行时错误)需要 Error Boundary。
beginWork
Suspense 组件会根据是否已捕获挂起(DidCapture 标记)或需要停留在 fallback 状态,决定本次渲染显示 fallback 还是 primary 内容:
- 若需显示 fallback,则创建 fallback 子树并将 primary 子树包裹为隐藏的
Offscreen组件以保留状态。 - 否则正常渲染 primary 子树。
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
completeWork
Suspense 组件负责完成水合收尾(处理 SSR 脱水节点)、处理 DidCapture 标记以触发重新渲染 fallback、调度重试队列(为等待的 Promise 附加 ping 监听器),并标记因 fallback/primary 切换而产生的副作用(如添加 Visibility 或 Passive 标记),最后向上冒泡 childLanes。
commit 阶段
Mutation 子阶段:
- 通过 Offscreen 组件的
Visibility标记,对 primary 树执行display: none(隐藏)或恢复显示。 - 处理边界删除时的清理工作(解绑 ref、调用
componentWillUnmount)。 - 清空已完成的重试队列。
Layout 子阶段:
- 执行
scheduleRetryEffect中调度的重试回调:为retryQueue中的每个 Promise 附加ping监听器(pingSuspendedRoot)。 - 允许子组件(primary 或 fallback)正常执行
useLayoutEffect和componentDidMount/Update。
Passive 阶段(异步):
- 执行 Offscreen 子树上因可见性变化而挂起的
useEffect清理和回调。
示例 代码分割
import { lazy, Suspense, useState } from "react";
const Card = lazy(() => import("./Card"));
const SuspenseB = () => {
const [num, setNum] = useState(0);
return (
<div className="suspense-b">
<p>num: {num}</p>
<button onClick={() => setNum(num + 1)}>click</button>
<Suspense fallback={<div className="suspense-b-fallback">Loading...</div>}>
<Card />
</Suspense>
</div>
);
};
export default SuspenseB;
beginWork updateSuspenseComponent
当前正在处理 workInprocess 是 suspense 组件,有属性pendingProps包含children 和 fallback,current为 null
![]()
![]()
mountSuspensePrimaryChildren
直接渲染 primary fiber
mountWorkInProgressOffscreenFiber 创建 Offscreen fiber
![]()
这里return,结束当前的beginWork
![]()
来到 beginWork updateOffscreenComponent
此时 workInProcess 为 Offscreen fiber,(之前 suspense要渲染的primary fiber),current 为 null
![]()
首次挂载创建 Offscreen 实例,用于存储 Offscreen 的可见性、待处理的标记、重试缓存及相关 transitions
![]()
![]()
reconcileChildren 子节点
createFiberFromTypeAndProps 创建 lazy fiber
return,又结束此次 beginWork
![]()
来到 beginWrok lazy fiber
![]()
在解析懒加载组件时,会 有微任务产生
进入 beginWork tag 为13 suspense
![]()
支持显示 fallback
挂载 primary fiber,mode 为隐藏状态
![]()
创建 fallback fiber,类型为 Fragment
![]()
关系
workInProcess.child 为 primary fiber
primary fiber的 sibling 为 fallback fiber
![]()
![]()
![]()
beginWork 结束,再次进入beginWork 处理 fallback fiber
当 lazy 加载完成后,继续处理
示例 数据获取 use
import { use, useState, Suspense } from "react";
const fetchData = (async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
return response.json();
})();
const SuspenseC = () => {
const result = use(fetchData);
const [count, setCount] = useState(0);
console.log("state-a-render-----");
return (
<div className="state-a">
<h3>StateA</h3>
<p>{result?.title}</p>
<p>当前count: {count}</p>
<button onClick={() => setCount(count + 1)}>SuspenseC -点击增加</button>
</div>
);
};
const App = () => {
return (
<Suspense fallback={<div className="suspense-c-fallback">Loading...</div>}>
<SuspenseC />
</Suspense>
);
};
export default App;