React 基础体系 · 第 32/70 篇。示例以 React 19、现代 TypeScript 和主流框架能力为基础;客户端与服务端边界会明确说明。
React lazy 与 Suspense:代码加载、边界、错误和用户体验
React.lazy 和 <Suspense> 经常同时出现,但它们解决的是两个不同层次的问题:
lazy把一个组件的代码加载过程转换为 React 可以观察的“未完成状态”;Suspense决定这个未完成状态应该在哪个 UI 边界显示什么;- 错误边界处理加载失败、渲染失败等异常;
- 过渡、预加载、缓存和边界位置则决定用户看到的是短暂占位、整页闪烁,还是连续可用的界面。
如果只把代码改成 lazy(() => import(...)),却没有理解边界、失败路径和状态变化,应用可能仍然出现白屏、整页闪烁、无限重试或难以诊断的加载错误。
一、先区分:代码加载、组件渲染和数据加载
浏览器执行 React 应用时,至少有三类不同的工作:
- 下载 JavaScript 代码:例如下载某个设置页面对应的 chunk;
- 执行组件函数并生成 React 元素;
- 获取组件需要的数据:例如请求用户设置、订单或报表。
React.lazy 主要处理第一类问题。它不会自动请求组件的数据,也不会把任意异步函数变成可被 Suspense 使用的数据资源。
const SettingsPage = lazy(() => import("./SettingsPage"));
这里的 import("./SettingsPage") 返回一个 Promise。打包工具通常会把 SettingsPage 拆成独立 chunk。当 React 第一次尝试渲染 SettingsPage 时,代码可能尚未下载完成,于是这个组件暂时不能完成渲染。
Suspense 处理的是“子树当前无法完成渲染”的状态,而不是专门处理网络请求:
<Suspense fallback={<PageSkeleton />}>
<SettingsPage />
</Suspense>
如果 SettingsPage 正在等待 lazy 模块,Suspense 会显示 fallback。如果组件已经加载完成但在 useEffect 中请求数据,普通情况下这个请求不会自动触发 Suspense;组件通常会自己维护 loading 状态。
因此,下面两种代码的语义不同:
// 代码分割:React.lazy + Suspense
const Chart = lazy(() => import("./Chart"));
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
// 普通客户端数据请求:需要自行维护状态
function UserPanel() {
const [state, setState] = useState<{
status: "loading" | "success" | "error";
user?: User;
error?: Error;
}>({ status: "loading" });
// useEffect 中请求数据,然后更新 state
}
React 生态中也存在基于 Suspense 的数据获取方案,但那是数据缓存层、框架或库提供的能力,不是 lazy 本身自动提供的能力。
二、React.lazy 的机制和约束
1. 基本形式
lazy 接收一个无参数函数。这个函数必须返回一个 Promise 或 thenable,其最终结果必须是一个包含 default 导出的模块对象。
import { lazy } from "react";
const ReportPage = lazy(() => import("./ReportPage"));
对应的模块应当类似:
// ReportPage.tsx
export default function ReportPage() {
return <h1>报表</h1>;
}
可以把它抽象成如下过程:
第一次渲染 lazy 组件
│
├─ 模块已完成:得到 default 组件,继续渲染
│
├─ 模块未完成:组件暂时挂起,向上寻找 Suspense
│
└─ Promise 拒绝:抛出加载错误,向上寻找 Error Boundary
lazy 会缓存加载结果,包括已解决的模块和已拒绝的 Promise。它不是每次渲染都重新执行 import()。这也是为什么它通常应定义在模块顶层,而不是组件函数内部。
错误示例:
function App() {
// 每次 App 重新渲染,都可能创建一个新的 lazy 组件类型
const Panel = lazy(() => import("./Panel"));
return <Panel />;
}
这会使 React 把重新创建的 Panel 视为不同的组件类型,导致子树卸载并重新挂载,状态可能丢失,甚至产生重复加载和难以理解的闪烁。
正确写法:
const Panel = lazy(() => import("./Panel"));
function App() {
return <Panel />;
}
这里有两个不同的“缓存”概念:
lazy缓存模块加载结果;- React 组件实例的状态依赖组件在树中的身份、位置和 key。
把 lazy 放在组件外部,首先是为了稳定组件类型;它不等于把业务数据永久缓存。
2. 命名导出不能直接作为 lazy 的结果
下面的模块只有命名导出:
// Chart.tsx
export function Chart() {
return <div>图表</div>;
}
直接这样写通常不符合 lazy 的要求:
const Chart = lazy(() => import("./Chart"));
因为模块对象没有 default 属性。可以在模块中增加默认导出:
export default function Chart() {
return <div>图表</div>;
}
也可以在适配函数中把命名导出映射成默认导出:
const Chart = lazy(() =>
import("./Chart").then((module) => ({
default: module.Chart,
})),
);
这个 then 的作用只是改变模块对象的形状,不是错误处理。若动态导入失败,Promise 仍然会拒绝,错误仍需由 Error Boundary 处理。
3. 动态导入失败的原因
import() 可能因为以下原因失败:
- 网络中断;
- CDN 或静态资源服务器返回 404;
- 发布后旧页面引用了已经被清理的 chunk;
- 内容安全策略或跨域配置阻止加载;
- 服务端返回了 HTML 而不是 JavaScript;
- 浏览器缓存和部署版本不一致。
失败时,Suspense 不会把错误自动变成“加载失败”文字。Suspense 只负责等待中的 UI;拒绝的 Promise 属于错误路径。
三、Suspense 是什么:可挂起子树的显示边界
Suspense 的核心不是“显示 loading”,而是定义一个边界:
<Suspense fallback={<Loading />}>
<SomeSubtree />
</Suspense>
当 SomeSubtree 在渲染过程中暂时无法完成时,React 可以渲染 fallback。这个“暂时无法完成”通常表现为组件向 React 抛出一个尚未完成的 thenable;使用 lazy 时,这个行为由 React 内部完成。
可以把它表示为状态转换:
stateDiagram-v2
[*] --> Rendering
Rendering --> Suspended: 子树等待未完成 thenable
Suspended --> Rendering: thenable fulfilled
Rendering --> Failed: 加载或渲染抛出错误
Failed --> Recovered: Error Boundary 恢复并改变子树
Rendering --> Revealed: 子树完成渲染
Revealed --> Suspended: 更新时再次挂起
Suspended --> Revealed: React 保留或恢复已显示内容
关键点是:Suspense 只观察它后代的挂起状态。
<Suspense fallback={<PageSkeleton />}>
<MainLayout>
<SettingsPage />
</MainLayout>
</Suspense>
如果 SettingsPage 挂起,这个边界可以显示 PageSkeleton。但如果边界本身放在 SettingsPage 内部:
function SettingsPage() {
const Panel = lazy(() => import("./Panel"));
return (
<Suspense fallback={<PanelSkeleton />}>
<Panel />
</Suspense>
);
}
这种写法还有前面提到的 lazy 定义位置问题,而且它只能覆盖 Panel,无法覆盖 SettingsPage 自身加载阶段。更合理的结构是把 lazy 组件放到模块顶层,并在调用方按用户体验选择边界范围。
四、边界位置决定用户看到什么
假设页面由导航、标题和主内容组成:
function App() {
return (
<AppLayout>
<Navigation />
<Suspense fallback={<FullPageSkeleton />}>
<ReportsPage />
</Suspense>
</AppLayout>
);
}
当 ReportsPage 的代码正在下载时,导航仍然可以保留,只有主内容显示占位。这通常比下面的整页边界更连续:
function App() {
return (
<Suspense fallback={<FullPageSkeleton />}>
<AppLayout>
<Navigation />
<ReportsPage />
</AppLayout>
</Suspense>
);
}
边界越外层,覆盖的子树越大,fallback 越可能造成整页替换;边界越内层,保留的已显示内容越多,但需要设计更多局部占位。
可以嵌套边界:
function ReportsPage() {
return (
<section>
<PageHeader />
<Suspense fallback={<SummarySkeleton />}>
<Summary />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
</section>
);
}
它表达了两个独立的加载区域:
Summary加载慢,不必隐藏PageHeader;SalesChart加载慢,不必隐藏Summary;- 两个区域可以分别显示符合最终布局的 skeleton。
这不是单纯的性能优化。边界实际上是 UI 故障和不确定性的隔离范围。
五、Suspense 不等于 Error Boundary
两者处理的状态不同:
| 状态 | 典型原因 | 负责显示什么 |
|---|---|---|
| 挂起 | lazy 模块尚未完成,或某个 Suspense 数据源尚未完成 | Suspense.fallback |
| 错误 | 动态导入拒绝、组件渲染抛错 | Error Boundary 的错误 UI |
| 成功 | 组件完成渲染 | 正常 UI |
一个可运行的错误边界可以这样写:
import { Component, type ErrorInfo, type ReactNode } from "react";
type ErrorBoundaryProps = {
children: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
};
type ErrorBoundaryState = {
error: Error | null;
};
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
render() {
if (this.state.error) {
return (
<section role="alert">
<h2>此区域加载失败</h2>
<p>{this.state.error.message}</p>
<button
type="button"
onClick={() => this.setState({ error: null })}
>
重试
</button>
</section>
);
}
return this.props.children;
}
}
然后组合使用:
import { lazy, Suspense } from "react";
import { ErrorBoundary } from "./ErrorBoundary";
const ReportsPage = lazy(() => import("./ReportsPage"));
export function ReportsRoute() {
return (
<ErrorBoundary
onError={(error, info) => {
console.error("Reports route failed", { error, info });
}}
>
<Suspense fallback={<ReportsSkeleton />}>
<ReportsPage />
</Suspense>
</ErrorBoundary>
);
}
这里的顺序很重要:
Suspense处理“还没准备好”;ErrorBoundary处理“已经失败”;- Error Boundary 包住 Suspense,能够接收 lazy 加载失败;
- 只有设置
error: null,边界才会尝试重新渲染子树。
一个按钮清除错误状态并不保证重试一定成功。由于 lazy 会缓存拒绝结果,重新渲染同一个 lazy 组件可能仍然立即失败。生产环境的重试策略要根据框架和资源加载器决定,例如:
- 先检测网络是否恢复;
- 对版本不匹配做一次受控的页面刷新;
- 由路由框架或模块加载器提供重新获取 chunk 的机制;
- 避免每次渲染自动重试,防止错误循环和请求风暴。
Error Boundary 也有边界。它通常不能捕获:
- 事件处理器中的错误;
- 异步回调中的错误,例如
setTimeout; - 服务端渲染阶段的所有错误;
- Boundary 自己抛出的错误。
事件处理器需要自己处理:
function SaveButton() {
const handleClick = async () => {
try {
await saveData();
} catch (error) {
console.error("保存失败", error);
}
};
return <button onClick={handleClick}>保存</button>;
}
六、更新时再次挂起:为什么页面会闪回 fallback
首次加载和更新时加载的用户感受不同。
首次进入报表页时,显示骨架屏通常合理:
<Suspense fallback={<ReportsSkeleton />}>
<ReportsPage />
</Suspense>
但如果用户已经看到了报表,点击筛选器导致新内容对应的代码或数据再次挂起,直接把整个已显示内容替换为 skeleton,界面会产生明显闪烁。
React 提供 startTransition,用于把某些更新标记为非紧急更新。对路由切换、筛选条件切换等场景,可以让 React 尽量保留当前可见内容,直到新内容准备好:
import { startTransition, useState } from "react";
function ReportTabs() {
const [tab, setTab] = useState<"sales" | "users">("sales");
function selectTab(nextTab: "sales" | "users") {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<button onClick={() => selectTab("sales")}>销售</button>
<button onClick={() => selectTab("users")}>用户</button>
<Suspense fallback={<ChartSkeleton />}>
{tab === "sales" ? <SalesChart /> : <UsersChart />}
</Suspense>
</>
);
}
这里需要区分两个事实:
startTransition不会加快网络下载;- 它改变的是更新的优先级和可见内容处理方式。
如果更新是由输入框控制,不能不加区分地把所有输入更新都放进 transition。文本输入需要立即反映用户按键;可以把输入状态保持为紧急更新,再把昂贵的派生视图交给 useDeferredValue 或单独的 transition。
七、一个完整的客户端代码分割示例
下面示例假设使用现代 TypeScript 和支持动态 import() 的打包工具,例如 Vite、Webpack 或对应的框架构建系统。
目录结构:
src/
main.tsx
App.tsx
ErrorBoundary.tsx
pages/
HomePage.tsx
SettingsPage.tsx
SettingsSkeleton.tsx
SettingsPage.tsx:
export default function SettingsPage() {
return (
<main>
<h1>设置</h1>
<p>这里是按需加载的设置页面。</p>
</main>
);
}
SettingsSkeleton.tsx:
export function SettingsSkeleton() {
return (
<main aria-busy="true" aria-label="设置正在加载">
<div className="skeleton title" />
<div className="skeleton line" />
<div className="skeleton line short" />
</main>
);
}
App.tsx:
import { lazy, Suspense, useState } from "react";
import { ErrorBoundary } from "./ErrorBoundary";
import { SettingsSkeleton } from "./pages/SettingsSkeleton";
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
function HomePage() {
return (
<main>
<h1>首页</h1>
<p>首页代码随初始入口一起加载。</p>
</main>
);
}
export default function App() {
const [route, setRoute] = useState<"home" | "settings">("home");
return (
<div>
<nav aria-label="主导航">
<button type="button" onClick={() => setRoute("home")}>
首页
</button>
<button type="button" onClick={() => setRoute("settings")}>
设置
</button>
</nav>
<ErrorBoundary
onError={(error, info) => {
// 生产环境应发送结构化错误、路由和构建版本信息
console.error("Route render/load error", { error, info, route });
}}
>
<Suspense fallback={<SettingsSkeleton />}>
{route === "home" ? <HomePage /> : <SettingsPage />}
</Suspense>
</ErrorBoundary>
</div>
);
}
运行时的步骤是:
- 初始入口加载
App和HomePage; - 用户点击“设置”;
- React 首次尝试渲染
SettingsPage; - 打包工具开始获取设置页面 chunk;
- chunk 未完成时,Suspense 显示
SettingsSkeleton; - chunk 成功后,React 使用模块的
default导出渲染页面; - chunk 失败时,错误向上到
ErrorBoundary; - 错误边界显示失败 UI,而不是继续显示加载骨架。
这个例子中的 Suspense 只覆盖当前内容区,而导航仍然保留。实际项目中,路由库通常会提供更完整的路由级 lazy 加载、预取、错误恢复和滚动恢复能力;手写路由时则必须自行决定这些行为。
八、用户体验不是“fallback 越漂亮越好”
fallback 的价值取决于它和最终布局的结构相似程度。下面的简单文字虽然可运行,但会造成内容尺寸跳变:
<Suspense fallback={<p>Loading...</p>}>
<SettingsPage />
</Suspense>
如果最终页面有固定标题、表格和按钮,加载期间最好保留接近的空间:
function SettingsSkeleton() {
return (
<main aria-busy="true">
<h1 className="placeholder-heading">设置</h1>
<div className="placeholder-panel" />
<div className="placeholder-panel" />
</main>
);
}
需要注意几个实际边界:
- skeleton 只是视觉占位,不代表资源已经加载;
aria-busy="true"可以向辅助技术表达区域仍在更新;- 如果 fallback 使用动画,应尊重
prefers-reduced-motion; - 加载时间较短时,立即显示复杂 spinner 可能比保留旧内容更扰人;
- 加载时间较长时,应提供明确的失败状态,而不是无限旋转;
- 骨架结构如果和真实布局差异过大,仍会产生布局偏移。
可以用 CSS 限制动画:
.skeleton {
background: #e5e7eb;
border-radius: 4px;
animation: pulse 1.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
}
}
代码分割也不是无条件收益。拆分 chunk 会减少初始 JavaScript,但会增加后续请求和加载时机的不确定性。如果一个组件几乎总是在首屏使用,把它拆成独立 chunk 可能只是把成本从初始阶段推迟到用户已经开始操作之后。是否拆分应结合页面访问路径、资源大小、缓存策略和真实监控数据判断,不能仅凭 chunk 数量下结论。
九、预加载、预取和 lazy 的启动时机
lazy(() => import("./SettingsPage")) 通常在组件真正需要渲染时启动动态导入。对于“用户已经点击后才开始下载”的页面,这可能产生可感知等待。
预加载可以在更早的时机启动,但要区分几个概念:
- 预加载:尽早获取资源,通常带有较强优先级;
- 预取:在预计未来可能使用时获取,优先级通常更低;
- 真正渲染:React 仍需在组件树中使用 lazy 组件,才能产生 UI。
具体预加载方式依赖打包工具、框架和路由库。不能假设所有 lazy 对象都有一个标准的 .preload() 方法;这不是 React.lazy 的通用 API。框架可能基于路由链接、鼠标悬停、视口可见性或服务器生成的资源提示实现预取,但应使用该框架明确提供的能力。
一个常见的经验策略是:用户聚焦到“设置”导航项时预取设置 chunk,真正点击时仍由 Suspense 负责显示安全的加载状态。预取失败不能代替点击时的错误处理,因为资源可能在预取后失效,或用户网络状态已经改变。
十、客户端与服务端边界
1. 客户端渲染
在纯客户端应用中,动态导入的 chunk 通常由浏览器在运行时请求。Suspense 负责在 React 树中显示 fallback,错误边界负责处理失败。
这要求构建系统能够:
- 识别动态
import(); - 生成可访问的 chunk;
- 在部署后保持入口文件、chunk 清单和 CDN 路径的一致性。
如果服务器部署后删除了旧 chunk,用户可能仍持有旧入口页面,点击某个路由时请求到 404。这是典型的“代码加载错误”,不是 React 组件逻辑错误。
2. 服务端渲染
现代 React 支持服务端流式渲染中的 Suspense 边界。服务端可以先输出已准备好的内容,并在异步子树完成后继续发送内容;客户端再进行 hydration。实际行为取决于使用的框架、渲染 API、组件类型和资源清单。
服务端 Suspense 的存在不意味着浏览器永远不需要下载 lazy chunk。客户端需要对交互组件进行 hydration,仍然需要相应的 JavaScript。
服务端环境还存在几个边界:
- 浏览器专用 API 不能在服务端渲染阶段直接使用;
- 服务端和客户端必须对组件树、边界和条件分支保持可协调;
- 动态导入的服务端 chunk 清单、客户端 chunk 映射和预加载提示通常由框架负责;
- 服务端渲染错误与客户端 hydration 错误的报告路径可能不同;
- Error Boundary 不能被当成服务端所有异常的统一兜底机制。
因此,在 Next.js、Remix、React Router 框架能力或其他 SSR 方案中,应遵循对应框架关于客户端组件、服务端组件、路由 lazy 和错误边界的约定。React API 本身提供的是机制,不会替框架完成资源清单、HTTP 缓存和部署版本协调。
3. 服务端组件不是 lazy 的同义词
服务端组件可以减少发送到浏览器的客户端 JavaScript,但它们解决的是组件执行位置和数据流问题;lazy 解决的是模块按需加载。两者可能同时出现在一个应用中,但不能把“服务端组件”理解为“自动代码分割”,也不能把 Suspense 理解为只服务于客户端。
十一、失败表现和诊断路径
表现一:一直显示 fallback
可能原因包括:
- 动态导入 Promise 没有完成;
- chunk 请求被阻止或返回错误;
- 子树中的其他 Suspense 数据源一直未完成;
- fallback 实际上被更外层边界覆盖;
- 组件条件逻辑不断切换,导致重复挂起。
诊断步骤:
- 在浏览器 Network 面板检查对应
.jschunk 的状态码和响应内容; - 确认响应不是 HTML 错误页;
- 查看 Console 是否有
ChunkLoadError、模块解析错误或 CSP 报错; - 在构建产物中确认该模块确实被生成;
- 检查部署系统是否保留了与入口 HTML 对应的 chunk;
- 确认
lazy定义在模块顶层; - 检查 Suspense 子树是否还包含其他会挂起的组件。
表现二:加载失败后点击重试仍然失败
这可能不是按钮逻辑错误,而是 lazy 已缓存拒绝的 Promise,或者服务器上的资源确实仍不可用。重试前应记录:
- 当前应用构建版本;
- chunk URL;
- 用户浏览器和网络状态;
- HTTP 状态码;
- 是否发生过部署切换。
如果确认是旧版本页面引用旧 chunk,受控刷新可能恢复,但刷新本身会丢失未保存状态,不能无条件执行。对表单、编辑器和支付流程,应优先提示用户并保留本地草稿,而不是静默刷新。
表现三:组件状态莫名丢失
首先检查:
- 是否在渲染函数内部创建了
lazy组件; - 是否改变了组件的
key; - Suspense fallback 出现时,是否实际上卸载了子树;
- 路由切换是否改变了组件身份;
- 初次挂起的组件是否尚未完成第一次挂载。
React 对“首次挂起且尚未完成挂载”的树可以直接放弃并在资源完成后重新尝试,因此不能把尚未成功提交的组件状态当作可靠的持久状态。需要跨加载过程保留的数据,应放在更稳定的父级、路由状态、外部 store 或持久化层中。
表现四:开发环境频繁加载或重复日志
开发模式可能启用更严格的检查、额外渲染或不同的缓存行为,不能直接把开发环境日志次数等同于生产请求次数。应同时检查:
lazy工厂是否在模块顶层;- 是否在 Strict Mode 下观察到额外开发行为;
- 是否有组件被反复挂载;
- 浏览器缓存是否关闭;
- 打包工具的 HMR 是否介入。
最终应以生产构建和真实浏览器 Network 数据验证资源请求行为。
十二、边界设计的推导原则
可以用一个简单模型理解边界选择。设页面子树为 ,某个 Suspense 边界覆盖的子树为 。
当 中任一后代挂起时,React 需要为 选择 fallback。于是:
- 越大,被 fallback 替换的已显示内容越多;
- 越小,保留的上下文越多,但边界数量和局部状态设计越复杂;
- 如果 包含导航、标题和主内容,主内容加载会导致导航一起消失;
- 如果只让图表属于 ,图表等待不会影响报表标题和筛选器。
因此,边界通常应放在“用户能独立理解、独立等待、独立失败”的 UI 区域外侧,而不是机械地每个组件包一层。这个判断同时考虑三个变量:
- 视觉连续性:哪些内容应该保持可见;
- 失败隔离:哪些内容失败时仍然可以使用;
- 状态归属:哪些状态应在 fallback 出现后继续保留。
例如,仪表盘中的“账户信息”“趋势图”“最近活动”可能适合三个局部边界;但一个只有单一内容的简单页面,整页边界反而更清晰。
十三、规范保证、实现差异和工程取舍
可以明确区分三层结论:
React API 层面的保证
lazy接收返回模块 Promise 的加载函数;- 模块结果需要提供
default组件; - Suspense 可以显示其后代挂起时的 fallback;
- 加载或渲染错误需要由错误处理机制接管,而不是由 fallback 自动处理;
- lazy 组件应保持稳定身份,通常定义在模块顶层。
打包工具和框架的常见实现
- 动态
import()通常被拆成独立 chunk; - 路由系统可能根据路由实现代码分割和预取;
- SSR 框架可能生成模块清单、流式输出和资源预加载提示;
- 部署平台可能通过文件名 hash 和 CDN 缓存控制 chunk 生命周期。
这些不是仅凭 React 就能保证的行为,配置错误时仍可能得到 404、缓存错配或 hydration 问题。
工程经验建议
- 按用户可感知的 UI 区域设计边界;
- 首次加载使用与最终布局接近的 skeleton;
- 更新阶段考虑 transition,避免已显示内容不必要地闪回;
- 为动态导入失败提供可理解的错误 UI;
- 为失败事件记录 chunk URL、版本和路由;
- 不要在组件函数中创建 lazy 类型;
- 不要因为“拆 chunk”本身就断言性能一定变好;
- 通过生产构建、网络面板和真实监控验证收益。
React.lazy 负责把“模块还没到”表达给 React;Suspense 负责把这种暂时不可用映射为一个 UI 边界;Error Boundary 负责把不可恢复或已失败的路径变成可诊断、可操作的界面。只有三者再结合稳定的组件身份、合理的边界粒度、过渡策略和部署资源管理,代码分割才会从一个语法改动变成完整的用户体验设计。
系列导航与关联阅读
- 系列入口:React 完整学习路线:从渲染与 Hooks 到服务端组件和生产架构
- 上一篇:React Portal:事件、焦点、层级、可访问性和弹窗架构
- 下一篇:React useSyncExternalStore:外部状态、一致快照、订阅和 SSR
官方资料
本文依据 React 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论