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 内容。
js
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

js
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状态

js
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 因任务立即挂起

找到边界

js
// 未挂起,正常渲染
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组件还是加载中

懒加载组件加载完成

加载完毕是一个函数组件

示例 命名导出组件的懒加载
js
const InfoCard = lazy(() =>
import("./Card").then((mod) => ({ default: mod.InfoCard })),
);
js
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
js
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}

completeWork 阶段
js
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
源码
js
const Uninitialized = -1; // 未初始化,未调用
const Pending = 0; // 加载中
const Resolved = 1; // 加载成功
const Rejected = 2; // 加载失败
js
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)捕获
js
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 子树。
js
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清理和回调。
示例 代码分割
js
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
js
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;