React Suspense 与并发特性
概述
React 的 Suspense 和 Concurrent Mode(并发模式)是 React 16 之后最重要的架构演进。它们解决的核心问题是:如何在保持 UI 响应的同时,处理异步数据和耗时的渲染任务。
传统的 React 渲染是"同步且不可中断"的。一旦开始渲染,必须等所有组件都渲染完才能响应用户交互。在数据量大或组件复杂时,这会导致界面卡顿——用户点击按钮,要等好几秒才能看到结果。
Suspense 和 Concurrent Mode 通过可中断的渲染架构从根本上改变了这一点。React 可以:
- 在渲染过程中"暂停",优先处理用户交互
- 在后台"预渲染"即将显示的内容
- 避免不必要的 loading 状态闪烁
理解这些机制,不仅是为了用好这些 API,更是为了理解 React 为什么要做这些改变。
Suspense 机制原理
问题的本质
在 Suspense 出现之前,处理异步数据是 React 的痛点。常见模式有:
1. 状态管理法(Loading State)
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
问题:每个需要数据的组件都要写一堆 loading/error 状态逻辑。
2. 外部状态管理法(Redux/Server State 库)
把数据放到外部 store,组件直接从 store 读取。减少了组件内的样板代码,但增加了全局复杂度。
Suspense 的核心思想
Suspense 的解决思路完全不同:不是让组件自己管理 loading 状态,而是让组件"声明"自己需要等待数据,然后让外层的 Suspense 边界统一处理 loading 状态。
<Suspense fallback={<Loading />}>
<UserProfile userId={id} />
</Suspense>
这个写法意味着:"如果 UserProfile 在渲染过程中需要暂停(等待数据),就显示 Loading,而不是让 UserProfile 自己处理 loading 状态"。
"Suspend" 意味着什么
当组件在渲染过程中需要"等待"某些异步资源时,它可以通过以下方式触发 Suspense:
- 抛出 Promise:数据获取库(如 Relay、React Query)在数据未就绪时抛出 Promise
- 使用
use()hook:React 官方提供的use()hook 可以读取 Promise 或 Context
function UserProfile({ userPromise }) {
// use() 会暂停渲染直到 Promise resolve
const user = use(userPromise);
return <div>{user.name}</div>;
}
// 使用
<Suspense fallback={<Loading />}>
<UserProfile userPromise={fetchUser(userId)} />
</Suspense>
Suspense 的工作流程
React 官方文档对 Suspense 的描述非常清晰:当组件"挂起"(suspend)时,React 会:
- 尝试渲染 children:React 正常渲染子组件
- 检测到 suspend:如果子组件在渲染中等待异步资源,React 知道它"suspended"
- 显示 fallback:立即显示
<Suspense fallback={...} />中的 fallback - 继续等待:同时,React 继续在后台处理异步操作
- 数据就绪后:自动重新渲染,这次 children 不再 suspend,fallback 被隐藏
<Suspense fallback={<BigSpinner />}>
<Dashboard />
</Suspense>
// Dashboard 内部
function Dashboard() {
// 这个会暂停渲染直到数据就绪
const recentPosts = use(fetchRecentPosts());
return (
<div>
<NewStats />
<Suspense fallback={<PostsGlimmer />}>
<Posts posts={recentPosts} />
</Suspense>
</div>
);
}
Suspense 触发的唯一来源
重要:React Suspense 只能通过以下方式触发:
- 数据获取库的 Suspense 支持:Relay、React Query(通过
suspense: true选项) - React.lazy():动态 import 的组件加载
- 使用
use()hook 读取未 resolved 的 Promise
以下情况不会触发 Suspense:
useEffect中的数据获取- 事件处理器中的数据获取
- 普通 Promise(不用
use())
嵌套 Suspense 与渐进加载
Suspense 支持嵌套,实现渐进加载:
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
// Dashboard 内部
function Dashboard() {
return (
<div>
{/* 这个 Suspense 在 Dashboard 内部 */}
<Suspense fallback={<StatsGlimmer />}>
<Stats />
</Suspense>
{/* 这个 Suspense 也是 */}
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</div>
);
}
加载顺序:
- 初始显示
<PageSkeleton /> - Dashboard 渲染到 Stats 和 Posts 时,它们各自触发 Suspense
- Stats 就绪后显示,Posts 继续显示
<PostsGlimmer /> - Posts 就绪后,显示完整内容
避免不必要的 Fallback 显示
默认情况下,Suspense 会在组件 suspend 时立即显示 fallback。但有时我们希望不显示 fallback,直接显示旧内容,直到新内容就绪。
这就是 useDeferredValue 的用武之地。
Concurrent Mode:并发渲染
什么是并发模式
Concurrent Mode 是 React 16 开始引入的新渲染架构。核心特性是可中断的渲染。
在 Concurrent Mode 下,React 的渲染过程不再是"一气呵成",而是可以被"打断"的。这意味着:
- React 可以在渲染中途暂停,优先处理用户输入
- 可以同时"预渲染"多个版本的 UI
- 可以延迟不紧急的更新,保证紧急更新的响应
为什么需要并发渲染
考虑一个场景:用户在一个搜索框输入文字,触发了大量搜索结果的重新渲染:
function SearchPage() {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<BigList items={filterItems(allItems, query)} />
</div>
);
}
传统模式下:
- 用户输入 "a"
- React 重新渲染,触发 BigList 过滤和渲染
- 整个渲染过程不能中断——用户在这几秒内无法点击任何按钮
- 输入框也无法响应下一次 onChange
并发模式下:
- 用户输入 "a"
- React 开始重新渲染
- 渲染可以被中断——如果用户继续输入 "ab",React 可以暂停当前渲染,优先处理新的输入
- 最终只渲染 "ab" 的结果,而不是先渲染 "a" 再渲染 "ab"
startTransition
startTransition 是 React 提供的 API,用于标记非紧急的更新。
import { startTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [deferredQuery, setDeferredQuery] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 紧急更新:反映用户输入
startTransition(() => {
setDeferredQuery(value); // 非紧急更新:触发大量计算
});
};
return (
<div>
<input value={query} onChange={handleChange} />
<BigList items={filterItems(allItems, deferredQuery)} />
</div>
);
}
关键点:
setQuery(value)是紧急更新——用户期望立即看到输入的字符setDeferredQuery(value)在startTransition内,是非紧急更新——可以延迟
useTransition
useTransition 是 startTransition 的 hook 版本,额外提供 isPending 状态:
import { useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [deferredQuery, setDeferredQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
setDeferredQuery(value);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{/* 用 isPending 显示加载状态 */}
{isPending ? <Spinner /> : <BigList items={filterItems(allItems, deferredQuery)} />}
</div>
);
}
isPending 在 transition 进行中为 true,可以用于:
- 显示 loading 指示器
- 禁用某些 UI 元素
- 提供视觉反馈
useDeferredValue
useDeferredValue 是另一个处理非紧急更新的 hook,适用于从 props 或 Hook 返回值获取值的场景:
import { useDeferredValue } from 'react';
function SearchPage({ query }) {
const deferredQuery = useDeferredValue(query);
return (
<div>
<InputDisplay value={query} />
<BigList items={filterItems(allItems, deferredQuery)} />
</div>
);
}
function InputDisplay({ value }) {
// value 是紧急渲染
// deferredQuery 是延迟渲染
return <div>{value}</div>;
}
useDeferredValue vs startTransition:
| 场景 | useDeferredValue | startTransition |
|---|---|---|
| 值来自 props/hook | ✅ | ❌ |
| 值来自 setState | ❌ | ✅ |
| 需要 isPending | ❌ | ✅(用 useTransition) |
并发渲染的可中断性
理解 Concurrent Mode 的关键是理解React 如何实现可中断渲染。这需要回顾 Fiber 架构。
Fiber 架构回顾
Fiber 是 React 16 引入的新协调器(Reconciler)架构。它的核心思想是将渲染工作拆分成可中断的工作单元。
在 React 15 及之前,React 使用递归调和(Reconciliation),整个组件树被同步遍历和渲染,无法中断。
Fiber 的关键数据结构:
function FiberNode(tag, pendingProps) {
// 关键字段
this.return = null; // 父 Fiber
this.child = null; // 子 Fiber
this.sibling = null; // 兄弟 Fiber
this.alternate = null; // work-in-progress branch
this.effectTag = null; // 副作用标记
this.pendingProps = pendingProps;
this.memoizedState = null;
}
Fiber 将工作分成两个阶段:
1. Render Phase(可中断)
- 确定需要创建/更新的 React 元素
- 可以被暂停和恢复
- 可能被丢弃并重新开始
2. Commit Phase(不可中断)
- 同步执行 DOM 变更
- 副作用(useEffect)在这里执行
- 必须完整执行,不能中断
优先级与调度
Fiber 使用 lanes(优先级车道)系统来管理更新优先级:
// 简化的优先级定义
const SyncLane = 0b0001; // 同步优先级:用户输入、点击
const InputContinuousLane = 0b0100; // 连续输入
const DefaultLane = 0b1000; // 默认优先级
const TransitionLane = 0b10000; // Transition 优先级
const IdleLane = 0b100000; // 空闲优先级
React 调度器(Scheduler)根据 lane 优先级决定:
- 哪个更新先处理
- 哪个更新可以被中断
- 何时让出主线程
正确使用 Suspense 和并发特性
不要用 Suspense 做所有事情
Suspense 适合数据获取和代码分割,不适合:
- 用户输入验证(应该同步处理)
- 表单提交(应该用 mutation)
- 动画(应该用 CSS 动画或 requestAnimationFrame)
组合使用 Suspense 和 startTransition
function ProfilePage({ userId }) {
const [tab, setTab] = useState('overview');
const [isPending, startTransition] = useTransition();
const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab);
});
};
return (
<div>
<Tabs value={tab} onChange={handleTabChange} />
{isPending ? (
<TabSkeleton />
) : (
<Suspense fallback={<TabContentSkeleton />}>
<TabContent tab={tab} userId={userId} />
</Suspense>
)}
</div>
);
}
避免 Suspense 导致的布局抖动
当 Suspense fallback 和实际内容高度差异大时,会出现布局跳动(layout shift)。解决方案:
- 使用骨架屏占位:让 fallback 高度接近实际内容
- 固定容器高度:用 CSS 避免布局变化
- 使用 useDeferredValue:避免 fallback 显示
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<BigList items={filterItems(allItems, deferredQuery)} />
</div>
);
}
嵌套 Suspense 的最佳实践
// 外层:处理整体页面的加载
<Suspense fallback={<PageSkeleton />}>
<Header />
{/* 中层:处理复杂组件的加载 */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
{/* 内层:处理数据列表 */}
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</Suspense>
层级越深,fallback 越具体,用户体验越平滑。
React 18 的并发特性
React 18 正式支持了并发特性,包括:
- Automatic Batching:所有状态更新自动批处理(包括 Promise、setTimeout)
- startTransition:标记非紧急更新
- useDeferredValue:延迟值更新
- Suspense on Server:服务端 Suspense 支持
- Streaming SSR with Suspense:流式服务端渲染
常见误区
误区 1:Suspense 是 loading state
Suspense 不是 loading state 的替代品。它是一种加载编排机制,用于处理组件树中异步资源的加载顺序。
误区 2:startTransition 让一切变快
startTransition 不加速任何东西。它只是改变优先级,让紧急更新先执行。
误区 3:用了 Concurrent Mode 就自动变快
Concurrent Mode 不是性能优化的银弹。它解决的是响应性问题(保持 UI 响应),不是计算效率问题。
延展阅读
- React 官方文档 — Suspense — Suspense 的完整参考
- React 官方文档 — startTransition — startTransition API 文档
- React 官方文档 — useDeferredValue — useDeferredValue 详解
- React 官方文档 — useTransition — useTransition 详解
- Lin Clark — A Cartoon Intro to Fiber — Fiber 架构的可视化讲解
- React 18 Documentation — React 18 新特性