Suspense 是 React 提供的用于声明式处理异步加载状态的组件。在 React 18 中,Suspense 的能力得到了质的飞跃——它不再仅用于懒加载组件,而是可以统一管理数据加载、代码分割以及任何异步操作的加载状态,真正实现了“把加载逻辑交给 React,把 UI 表达交给开发者”。
Suspense 的核心概念
Suspense 的工作方式很简单:当子组件中的异步操作尚未完成时,Suspense 自动展示一个 fallback UI;一旦依赖的异步操作全部就绪,它便无缝切换回真实内容。 你不需要手写 if (isLoading) 逻辑,一切交由 Suspense 边界管理。
<Suspense fallback={<Loading />}>
<SomeAsyncComponent />
</Suspense>
SomeAsyncComponent 内部的异步操作(例如数据获取)一旦抛出 promise(在 Suspense 的上下文中,这是合法的“挂起”信号),React 就会捕获该操作并等待其 resolve,期间显示 fallback。
React 18 带来的关键增强
在 React 18 之前,Suspense 主要用于 React.lazy 代码分割,且仅在客户端生效。React 18 引入了并发渲染,使得 Suspense 可以在以下场景中发挥作用:
- 数据获取:与支持 Suspense 的数据库(如 React Query、Relay 等)配合,在组件内部直接引发数据挂起。
- 流式 SSR:服务端渲染时,Suspense 边界可以包裹部分内容,React 会先发送已就绪的 HTML,未就绪的部分会被自动替换为 fallback,待数据到达后再通过流式更新填充真实内容。
- 更精细的加载控制:通过嵌套 Suspense 边界,实现不同粒度的加载状态,避免“全屏 loading”的糟糕体验。
代码分割:组件级别的懒加载
这是 Suspense 最经典的用法。使用 React.lazy 动态导入组件,配合 Suspense 提供加载状态。
import { lazy, Suspense } from 'react';
const ProfilePage = lazy(() => import('./ProfilePage'));
const SettingsPage = lazy(() => import('./SettingsPage'));
function App() {
return (
<div>
<Suspense fallback={<div className="skeleton">加载中...</div>}>
<ProfilePage />
</Suspense>
<Suspense fallback={<div className="skeleton">设置加载中...</div>}>
<SettingsPage />
</Suspense>
</div>
);
}
每个 lazy 组件都有自己的 Suspense 边界,加载时互不阻塞,用户可以及时与已加载的部分交互。
数据加载:告别“加载中”样板代码
传统的条件渲染模式需要手动维护 loading 状态:
function UserList() {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
return <List data={users} />;
}
当数据依赖复杂、多层嵌套时,loading 逻辑会迅速扩散,而且瀑布式数据请求(先加载 A,再加载 B)很难优雅处理。
支持 Suspense 的数据方案(以 React Query 为例)可以这样写:
import { useQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
function UserList() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
suspense: true, // 开启 Suspense 模式
});
return <List data={users} />;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserList />
</Suspense>
);
}
UserList 内部不再需要 if (loading),因为当数据还在请求中时,React 会自动显示 Spinner;数据到达后直接渲染 List。多个组件可以共享一个 Suspense 边界,也可以各自拥有独立的边界。
嵌套 Suspense 边界:精细控制加载粒度
一个页面可能包含多个独立的异步模块,将它们包裹在不同的 Suspense 中,可实现局部加载效果:
<Suspense fallback={<BigSpinner />}>
<Header />
<Suspense fallback={<PostSkeleton />}>
<PostContent />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</Suspense>
当 PostContent 请求数据时,只会在对应区域显示骨架屏,Header 和已就绪的 Comments 不受影响。React 会按需独立地挂起/恢复每个边界,极大提升用户体验。
流式 SSR 中的应用
React 18 支持流式 SSR,配合 Suspense 可以实现“边渲染边传输”。服务端渲染时,包裹在 Suspense 中的异步组件并不会阻塞整个页面的生成。React 先将其他部分的 HTML 发送给浏览器,待异步组件就绪后,再以流式方式推送其 HTML 和相关的 hydration 脚本。
// 服务端组件示例
function ServerApp() {
return (
<html>
<head>...</head>
<body>
<Header />
<Suspense fallback={<div>加载评论...</div>}>
<Comments />
</Suspense>
<Footer />
</body>
</html>
);
}
浏览器会收到完整的 <head>、<Header /> 和 <Footer /> 的 HTML,用户可以立即看到页面框架,而评论区域先显示 fallback 文本,稍后 React 再注入真实的评论 HTML。这种方式消除了传统 SSR 必须等所有数据就绪才能响应的问题,显著缩短首屏时间(TTFB)和可交互时间(TTI)。
最佳实践与注意事项
- 选择合适的边界粒度:避免将所有异步内容全部包在一个大的 Suspense 下,否则一个慢请求会“拖累”整个页面。合理拆分边界,让各部分独立加载。
- fallback 设计要有意义:建议使用骨架屏而不是简单的“加载中”文字,减少布局跳动,提供更好的视觉连续性。
- 错误边界配合使用:异步操作可能失败,在 Suspense 外包裹错误边界(Error Boundary),可以优雅地捕获异常并显示降级 UI,避免白屏。
- 并非所有数据库都原生支持 Suspense:React Query、SWR、Relay 等已经很好地支持,但若自行封装数据请求,需要遵循 Suspense 的 promise throw 协议,或使用
useHook(React 19 支持)来消费 promise。在实际项目中,建议优先选择成熟的 Suspense 生态库。
- 注意并发特性下的行为:React 18 并发模式下,Suspense 的挂起和恢复过程是可中断的,React 可能尝试多次渲染才最终提交。通常用户不会感知到中间状态,但要确保副作用代码(如数据拉取)是幂等的,且不应依赖渲染次数。
小结
React 18 的 Suspense 已经从一个组件懒加载的附属品,成长为处理所有异步状态的声明式利器。它统一了数据加载和代码分割的加载态处理,配合并发渲染和流式 SSR,让开发者可以以更自然的方式编写加载、骨架屏、部分加载等复杂逻辑,真正实现了“UI 声明式”的闭环。掌握 Suspense,是进入现代 React 开发模式不可或缺的一步。