
一句话:Suspense 不只是「显示 fallback」——它背后是一套调度机制:挂起会被登记为「挂起的车道(suspendedLanes)」并监听 promise 完成(ping),而是否显示 fallback 取决于「这次更新是不是 transition」(transition 中挂起会保留旧内容)。
一、「挂起」从哪里来
// ① React.lazy:代码还没下载完
const Heavy = lazy(() => import('./Heavy'));
// ② use(promise):数据还没就绪(React 19)
function Comments({ promise }: { promise: Promise<Comment[]> }) {
const comments = use(promise); // ⭐ 未就绪 → 挂起
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
// ③ 数据层抛 promise:useSuspenseQuery / 框架的 loader
function Profile() {
const { data } = useSuspenseQuery({ queryKey: ['me'], queryFn: fetchMe }); // 内部会 throw promise
return <div>{data.name}</div>;
}
⭐⭐ 三者本质完全一样:**在渲染过程中「抛出一个 thenable(promise)」**。
这是 Suspense 的唯一协议:
· 组件在 render 期间 `throw promise`
· React 捕获它(不是当作错误!而是「挂起信号」)
· React 找到「最近的 Suspense 边界」→ 显示它的 fallback
· 同时给这个 promise 挂一个「完成回调」(ping)
· promise resolve → ping → React 重新渲染那个边界
⭐ 为什么用「抛出」而不是「返回 loading 状态」?
因为「抛出」能让 React **在渲染中途放弃这棵子树**(不产生半成品输出),
同时把「等待」变成一个「可以被调度系统处理的信号」——
这样 React 才能做「优先级」与「不立刻显示 fallback」这些事。
// ✅ 手写一个「最小可用的挂起数据源」(理解协议)
function createSuspenseResource<T>(promise: Promise<T>) {
let status: 'pending' | 'success' | 'error' = 'pending';
let result: T;
let error: unknown;
const suspender = promise.then(
(value) => { status = 'success'; result = value; },
(err) => { status = 'error'; error = err; }
);
return {
read(): T {
if (status === 'pending') throw suspender; // ⭐ 挂起(抛 promise)
if (status === 'error') throw error; // ⭐ 错误(抛 error)
return result;
},
};
}
// 使用
const userResource = createSuspenseResource(fetchUser());
function User() {
const user = userResource.read(); // 未就绪时这里会「抛」
return <div>{user.name}</div>;
}
二、挂起后 React 做了什么
⭐⭐ 挂起后的四件事(顺序很重要):
① 【登记「挂起的车道」】
把「这次更新所在的车道」标记为 suspendedLanes
→ 意思是「这条车道的渲染被阻塞了,在它就绪前别重试」
② 【监听 promise 完成(ping)】
给 promise 挂一个回调:完成时把对应车道加进 pingedLanes
→ 并触发一次「重试」(retryTimedOutBoundary / pingedLanes 被消费)
③ 【处理「已经渲染出来的部分」】
· 如果这个边界「之前显示过内容」,React 会把那部分**隐藏**(Offscreen 机制,见 4.13)
· 如果这个边界「从未显示过」,就没有旧内容可隐藏
→ 所以「隐藏」与「显示 fallback」是两件事
④ 【决定「显示谁」】⭐ 关键分支(见下一节)
· 这次更新是 transition?→ **保留旧内容**(不显示 fallback)
· 不是 transition? → **显示 fallback**
⭐ 为什么要有 pingedLanes 这套机制(而不是「promise.then 后直接 setState」)?
因为「promise 完成」不是一个 React 事件——
它需要被「翻译成一次调度」(在合适的优先级、合适的时机重渲染)。
`pingedLanes` 就是「把这个外部信号接入 React 调度系统」的桥。
三、是否显示 fallback:靠什么决定
// ❌ 非 transition:挂起立刻显示 fallback(内容会「闪一下骨架」)
function Search({ keyword }) {
const [results, setResults] = useState([]);
const handleChange = (e) => {
setKeyword(e.target.value);
setResults(null); // 触发同步更新 → 子树挂起 → 立刻显示 fallback
};
return (
<Suspense fallback={<Skeleton />}>
<Results resource={results} />
</Suspense>
);
}
// ✅ transition:挂起时【保留旧内容】,等新内容就绪再一次性切换
function Search() {
const [keyword, setKeyword] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
startTransition(() => setKeyword(value)); // ⭐ 挂起发生在 transition 里
};
return (
<Suspense fallback={<Skeleton />}>
<Results keyword={keyword} /> {/* 挂起时不显示 fallback,保留上一次的结果 */}
</Suspense>
);
}
⭐⭐⭐ 这是 Suspense 最关键的一条规则(面试必答):
「如果挂起来自【transition(startTransition / useTransition / useDeferredValue)】,
React 不会显示 fallback —— 它会【继续显示之前的内容】,直到新内容就绪。」
「如果挂起来自【非 transition 的更新】(普通 setState、首次渲染),
React 会显示 fallback。」
⭐ 为什么这样设计?
· 用户主动触发的(点击/输入)应该「立刻有反馈」→ 该显示什么就显示什么
· 但「切 Tab、切换筛选」这类「意图明确、结果未就绪」的场景,
显示骨架反而更糟(页面闪一下)→ 保留旧内容更连贯
⭐ 实践推论:
「Tab 切换 / 路由切换不闪骨架」的正确写法不是「给 Suspense 加延迟」,
而是「把这次更新放进 transition」。
| 场景 | 挂起来源 | 是否显示 fallback | 用户感受 |
|---|---|---|---|
| 首次进入页面 | 普通渲染 | ✅ 显示 | 正常的加载态 |
| 点击「切换筛选」→ 数据未就绪 | transition | ❌ 不显示(保留旧结果) | 内容「稍后更新」,不闪 |
| 点击「加载更多」→ 请求挂起 | 普通更新 | ✅ 显示 | 底部 loading |
| 快速输入搜索词 | transition | ❌ 不显示 | 输入跟手,结果稍后 |
useDeferredValue 的滞后值 |
transition | ❌ 不显示 | 内容平滑跟上 |
// ✅ 一个完整例子:路由切换「不闪骨架」
function Router() {
const [pathname, setPathname] = useState('/');
const [isPending, startTransition] = useTransition();
const navigate = (to: string) => {
startTransition(() => setPathname(to)); // ⭐ 关键:放进 transition
};
return (
<>
{isPending && <TopProgressBar />} {/* 用「轻提示」代替骨架 */}
<Suspense fallback={<PageSkeleton />}>
<Route path={pathname} />
</Suspense>
</>
);
}
// ⭐ 对比:如果 navigate 里直接 setPathname(to),切换时就会「闪一下骨架」
四、revealOrder:多个边界怎么揭示
// 默认:多个兄弟边界「各自独立」揭示 —— 谁先就绪谁先显示
<Suspense fallback={<ASkeleton />}><A /></Suspense>
<Suspense fallback={<BSkeleton />}><B /></Suspense>
<C /> {/* 非挂起内容,立刻显示 */}
// → 如果 A 比 B 先就绪,用户会看到 A 先出现、B 后出现(可能「跳一下」)
// ⭐ SuspenseList(实验特性):控制「揭示顺序」
import { SuspenseList } from 'react';
<SuspenseList revealOrder="forwards"> {/* 按顺序揭示:A 好了才揭示 B */}
<Suspense fallback={<ASkeleton />}><A /></Suspense>
<Suspense fallback={<BSkeleton />}><B /></Suspense>
<Suspense fallback={<CSkeleton />}><C /></Suspense>
</SuspenseList>
<SuspenseList revealOrder="together"> {/* 一起揭示:全部就绪才一起显示 */}
…
</SuspenseList>
<SuspenseList revealOrder="backwards"> {/* 从后往前揭示 */}
…
</SuspenseList>
revealOrder |
行为 | 适合 |
|---|---|---|
forwards |
按顺序揭示(前一个就绪后才揭示下一个) | 有阅读顺序的内容(文章段落) |
backwards |
从后往前揭示 | 少见(列表追加) |
together |
全部就绪后一起揭示 | 表格的多列数据(避免「参差不齐」) |
| 默认(不用 SuspenseList) | 各自独立,先到先显示 | 大多数场景 |
⚠️ 现状:`SuspenseList` 目前仍是【实验特性】(且在新的并发实现下行为有变化)。
实践中的替代手段是「把「需要一起显示」的内容放进同一个 Suspense 边界」
—— 同一边界内的挂起会一起等,从而「一起揭示」。
五、SSR 与客户端:两条不同的路径
| 维度 | 客户端 Suspense | SSR / 流式 Suspense |
|---|---|---|
| 「挂起」的表现 | 用 Offscreen 隐藏已渲染部分 + 显示 fallback | 服务端先输出 fallback 的 HTML + 一个占位标记 |
| 就绪后的行为 | React 重新渲染该边界(保留 state) | 服务端把真实 HTML 流式发送 + 一段替换脚本 |
| 是否显示 fallback | 取决于是否 transition | 总是先输出 fallback(因为 HTML 必须有序) |
| 与「水合」的关系 | 挂起不影响外层水合 | 慢的部分后水合(渐进式水合) |
⭐⭐ 一个「反直觉」但重要的差异:
客户端:挂起「可能不显示 fallback」(transition 场景)
服务端:**总是**先输出 fallback 的 HTML(因为 HTML 流必须有序,
没有「保留旧内容」这个选项——第一次访问时也没有旧内容)
⭐ 所以「SSR 下的 transition」并不能避免骨架出现(首屏本来就要给骨架),
transition 主要改善的是【客户端导航/交互】时的骨架闪烁。
六、完整实现:一个 mini Suspense(190 行)
下面是「用类组件捕获抛出的 promise」实现的 Suspense(这是社区 polyfill 的思路),并且实现了「节流」与「transition 下不显示 fallback」两个关键特性。
// mini-suspense.tsx —— 教学版 Suspense
import React, { Component, createContext, useContext, useRef, useEffect, useState, useTransition } from 'react';
/** ============ 1. 工具:判断是否为 thenable ============ */
function isThenable(value: unknown): value is Promise<unknown> {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as { then?: unknown }).then === 'function'
);
}
/** ============ 2. 调度上下文:让「transition 内」的挂起不显示 fallback ============ */
interface RuntimeContext {
/** 当前是否处于「低优先级(transition)」更新中 */
isTransition: boolean;
/** 注册一个「重试回调」——当挂起的 promise 完成时被调用 */
ping: (fn: () => void) => void;
}
const Runtime = createContext<RuntimeContext>({ isTransition: false, ping: (fn) => fn() });
export const useMiniRuntime = () => useContext(Runtime);
/** ============ 3. 提供 transition 能力的包装 ============ */
export function MiniRoot({ children }: { children: React.ReactNode }) {
const [isPending, startTransition] = useTransition();
const pendingRef = useRef(0);
const ping = (fn: () => void) => {
// ⭐ 关键:用 transition 触发「重试」,
// 这样「重试期间的挂起」也不会显示 fallback
pendingRef.current++;
startTransition(() => {
fn();
pendingRef.current--;
});
};
return (
<Runtime.Provider value={{ isTransition: isPending || pendingRef.current > 0, ping }}>
{children}
</Runtime.Provider>
);
}
/** ============ 4. Suspense 本体 ============ */
interface MiniSuspenseProps {
fallback: React.ReactNode;
children: React.ReactNode;
/** ⭐ 节流:挂起后「延迟多久」才显示 fallback(避免一闪而过) */
throttleMs?: number;
}
interface MiniSuspenseState {
/** 是否处于「挂起」状态 */
suspended: boolean;
/** 是否「到了该显示 fallback」的时刻(节流用) */
showFallback: boolean;
error: unknown;
}
export class MiniSuspense extends Component<MiniSuspenseProps, MiniSuspenseState> {
static contextType = Runtime;
declare context: RuntimeContext;
state: MiniSuspenseState = { suspended: false, showFallback: false, error: null };
private throttleTimer: ReturnType<typeof setTimeout> | null = null;
private retryCount = 0;
/**
* ⭐ 核心:捕获子组件渲染时「抛出的 thenable」
* 注意:真实 React 是在 reconciler 里捕获的;
* 这里借用 Error Boundary 的机制来「捕获并重试」(polyfill 思路)
*/
static getDerivedStateFromError(error: unknown): Partial<MiniSuspenseState> | null {
if (isThenable(error)) return { suspended: true, error: null };
return { error }; // 真正的错误 → 交给错误边界逻辑
}
componentDidCatch(error: unknown) {
if (!isThenable(error)) throw error; // 真错误往上抛(交给外层 ErrorBoundary)
const thenable = error as Promise<unknown>;
// ⭐ ① 「节流」:如果不是 transition,延迟 throttleMs 再显示 fallback
if (this.throttleTimer === null) {
if (!this.context.isTransition) {
this.throttleTimer = setTimeout(() => {
this.throttleTimer = null;
if (this.state.suspended) this.setState({ showFallback: true });
}, this.props.throttleMs ?? 120);
} else {
// ⭐ transition 内:不显示 fallback(保持旧内容)
this.setState({ showFallback: false });
}
}
// ⭐ ② 监听 promise 完成 → ping → 重试渲染
thenable.then(
() => this.retry(),
(err) => this.setState({ suspended: false, error: err })
);
}
componentWillUnmount() {
if (this.throttleTimer) clearTimeout(this.throttleTimer);
}
private retry = () => {
this.retryCount++;
this.context.ping(() => {
// 清掉节流计时器,回到「未挂起」状态 → 触发重新渲染(子组件会再试一次)
if (this.throttleTimer) {
clearTimeout(this.throttleTimer);
this.throttleTimer = null;
}
this.setState({ suspended: false, showFallback: false, error: null });
});
};
render() {
const { suspended, showFallback, error } = this.state;
const { children, fallback } = this.props;
if (error) throw error; // 交给外层 ErrorBoundary
// ⭐ 关键:挂起但「还没到显示 fallback 的时刻」→ 保持旧内容
// (真实 React 用 Offscreen 保留 DOM;这里用「保留上一次的 children 输出」来近似)
if (suspended && !showFallback) {
return this.lastChildren ?? null; // 保留上一帧的输出
}
if (suspended && showFallback) {
return <>{fallback}</>; // 显示 fallback
}
// 未挂起:渲染子组件,并记住输出(供「挂起时保留」用)
this.lastChildren = children;
return <>{children}</>;
}
private lastChildren: React.ReactNode = null;
}
/** ============ 5. 一个「会挂起」的数据源 + 使用示例 ============ */
const cache = new Map<string, { status: 'pending' | 'done'; value?: string; promise: Promise<void> }>();
function fetchSlow(key: string, delayMs: number) {
let entry = cache.get(key);
if (!entry) {
const promise = new Promise<void>((resolve) => setTimeout(resolve, delayMs)).then(() => {
entry!.status = 'done';
entry!.value = `「${key}」的结果(耗时 ${delayMs}ms)`;
});
entry = { status: 'pending', promise };
cache.set(key, entry);
}
return entry;
}
function SlowData({ id, delay }: { id: string; delay: number }) {
const entry = fetchSlow(id, delay);
if (entry.status === 'pending') throw entry.promise; // ⭐ 挂起
return <p>{entry.value}</p>;
}
export function Demo() {
const [id, setId] = useState('A');
return (
<MiniRoot>
<div>
<button onClick={() => setId('A')}>切到 A(快)</button>
<button onClick={() => setId('B')}>切到 B(慢 800ms)</button>
<button onClick={() => { cache.clear(); setId('C'); }}>切到 C(慢,清缓存)</button>
{/* ⭐ 用 throttleMs 观察「节流」效果;用 transition 观察「不显示 fallback」 */}
<MiniSuspense fallback={<p>加载中…</p>} throttleMs={150}>
<SlowData id={id} delay={id === 'A' ? 100 : 800} />
</MiniSuspense>
</div>
</MiniRoot>
);
}
⭐⭐ 这个实现复现了 Suspense 的三个关键特性:
① 【throw thenable = 挂起】
`if (entry.status === 'pending') throw entry.promise;`
→ 边界捕获 → 显示(或延迟显示)fallback
② 【ping + 重试】
promise resolve 后调用 retry → 触发一次 transition 更新 → 子组件重新渲染
(真实 React 是「给车道打 pinged 标记 → 调度重试」)
③ 【节流 + transition 下不显示 fallback】
· throttleMs:延迟显示 fallback,避免「一闪而过」
· isTransition:transition 中挂起 → 直接不显示 fallback
⚠️ 与真实实现的差别:
· 真实 React 在 reconciler 内部捕获(不是靠 ErrorBoundary)
· 真实 React 用 Offscreen「保留 DOM 并隐藏」,这里只是「保留上一帧的元素」
· 真实 React 会「丢弃半成品子树」,这里有更粗糙的重试
· 真实的优先级与车道机制比「一个 boolean」复杂得多
七、本篇特有的坑
// ① 以为「Suspense 加个延迟就能避免骨架闪烁」
<Suspense fallback={<Skeleton />}>…</Suspense> // ⚠️ 延迟只是掩盖
// ✅ 正确做法:把「切换」放进 transition(startTransition / useTransition)
// ② 在 transition 外的普通 setState 触发挂起 → 一定显示 fallback
const handleTab = (t) => setTab(t); // ⚠️ Tab 切换会闪骨架
const handleTab2 = (t) => startTransition(() => setTab(t)); // ✅ 不闪
// ③ 让「数据源」每次渲染都新建 promise → 无限挂起
throw fetchData(); // ❌ 每次渲染都抛新 promise
throw cachedResource.read(); // ✅ 用缓存/资源对象
// ④ 把「真错误」当成「挂起」抛(Suspense 只认 thenable)
throw new Error('x') 在 Suspense 边界内 → 不会显示 fallback,
必须由 ErrorBoundary 接(见 8.3)
// ⑤ 把 ErrorBoundary 放在 Suspense 内层(顺序反了)
<Suspense fallback={…}><ErrorBoundary fallback={…}>…</ErrorBoundary></Suspense>
// ⚠️ 语义混乱(错误 UI 会被当成「未就绪的内容」)
// ✅ ErrorBoundary 在外、Suspense 在内
// ⑥ 在「Suspense 边界之外」读一个会挂起的资源
function App() {
const data = use(promise); // ❌ 没有 Suspense 包裹 → 报错「A component suspended…」
return <div>{data}</div>;
}
// ⑦ 用 Suspense 包「不是真正异步」的东西(同步组件永远不会挂起)
<Suspense fallback={<Skeleton />}><SyncComponent /></Suspense> // ⚠️ fallback 永不出现
// ⑧ 「多个边界」都放同一个 fallback → 用户看到「骨架海」
// ✅ 合并「同时就绪」的内容到同一个边界,或给每个边界更贴合的骨架
// ⑨ 依赖「Suspense 一定保留子组件 state」
// ⚠️ 挂起时「已经渲染的部分」是隐藏而非卸载,所以 state 保留(这点是对的);
// 但如果「挂起发生在首次渲染」→ 没有 state 可保留
// ⑩ 在 SSR 中期待「transition 能避免骨架」
// ⚠️ 服务端总是先输出 fallback(HTML 流必须有序)→ 首屏一定有骨架
// ⑪ 用 Suspense 做「错误处理」
// ⚠️ 它只处理「等待」;错误要 ErrorBoundary(Promise reject 会抛错)
// ⑫ 忘记「挂起的 promise 会被『多次订阅』」
// ⚠️ 同一次渲染里多个组件读同一个资源 → 要给「同一个 promise」挂多次监听
// (真实 React 用 ping 去重;手写实现要注意「只 retry 一次」)
// ⑬ 在「Suspense 边界内」做「必须同步完成的事」(如测量布局)
// ⚠️ 挂起时这部分不会执行 → 依赖它的逻辑要放在边界之外或等就绪
// ⑭ 认为「Suspense 能提升性能」
// ⚠️ 它不减少工作量;它改善的是「感知」与「调度粒度」(让慢的部分不阻塞快的)
// ⑮ 在「非并发模式」(legacy ReactDOM.render)下期待新语义
// ⚠️ transition 与 Suspense 的很多行为依赖 createRoot
// ③ 的完整修复:把 promise 缓存起来(资源模式)
const resourceCache = new Map<string, Promise<Data>>();
function getResource(id: string): Promise<Data> {
let p = resourceCache.get(id);
if (!p) {
p = fetchData(id);
resourceCache.set(id, p);
}
return p; // ⭐ 同一个 id 永远返回同一个 promise
}
function Profile({ id }: { id: string }) {
const data = use(getResource(id)); // ✅ promise 稳定
return <div>{data.name}</div>;
}
// ⑫ 的说明:手写实现要避免「重复 retry」
private retry = () => {
// ⭐ 用「是否已挂起」作为去重标记:一次挂起只对应一次 retry
if (!this.state.suspended) return;
this.context.ping(() => this.setState({ suspended: false, showFallback: false }));
};
八、面试延伸
- 「Suspense 的挂起机制是什么?」
唯一的协议是「组件在渲染期间抛出一个 thenable」。React 捕获它(不是当作错误,而是「挂起信号」),然后:① 找到最近的 Suspense 边界,决定显示 fallback 还是保留旧内容;② 把这个 promise 登记为「挂起」(标记suspendedLanes)并挂一个完成回调(ping);③ promise resolve 时通过pingedLanes把信号接回 React 的调度系统,触发重试渲染。所以「挂起」本质是「一个能被调度系统处理的外部等待信号」。 -
「什么时候显示 fallback,什么时候不显示?」
关键规则:取决于「这次更新是不是 transition」。① 是 transition(startTransition/useTransition/useDeferredValue)→ React 不显示 fallback,而是继续显示之前的内容,直到新内容就绪后一次性切换;② 不是 transition(普通setState、首次渲染)→ 显示 fallback。设计理由是「用户主动触发的交互应该立刻有反馈,而『切换 Tab / 筛选』这类『意图明确、结果未就绪』的场景,保留旧内容比闪骨架更连贯」。 -
「为什么要『节流』?」
因为「挂起时间很短」时显示 fallback 会一闪而过(内容 → 骨架 → 内容),体验比「什么都不显示」更差。所以 React 有一套节流策略:短暂挂起(在阈值内)不显示 fallback(表现为「内容稍晚一点更新」)。注意:节流是「避免闪烁的兜底」,而避免切换闪烁的正确手段是「把更新放进 transition」——两者是不同层面的东西。 -
「
suspendedLanes和pingedLanes是什么?」
它们是「挂起状态在调度系统里的表示」:①suspendedLanes——记录「哪些优先级的更新被挂起阻塞了」(在它们对应的 promise 完成前,React 不会白白重试它们);②pingedLanes——当某个挂起的 promise 完成时,把对应车道标记为「已 ping」,从而允许 React 重试这条车道的渲染。这一对标记是「把外部的异步信号翻译成 React 内部的调度语言」的桥梁。 -
「多个 Suspense 边界怎么决定揭示顺序?」
默认情况下「各自独立」——谁先就绪谁先显示,可能出现「参差不齐」的观感。要控制顺序可以用<SuspenseList revealOrder="forwards | backwards | together">:forwards按顺序揭示、together全部就绪才一起显示、backwards从后往前。但SuspenseList目前仍是实验特性,实践中的替代手段是「把『需要一起显示』的内容放进同一个 Suspense 边界」(同一边界内会一起等)。 -
「SSR 的 Suspense 和客户端的有什么不同?」
关键差异是「服务端总是先输出 fallback 的 HTML」——因为 HTML 流必须有序,服务端没有「保留旧内容」这个选项(首次访问也没有旧内容)。而客户端的挂起「可能不显示 fallback」(transition 场景,保留已渲染的 DOM)。推论:在 SSR 下,transition不能避免首屏骨架出现(首屏本来就要给骨架),它主要改善的是「客户端交互/导航」时的骨架闪烁。此外服务端的慢边界会「后水合」(渐进式水合)。 -
「Suspense 和 ErrorBoundary 的关系?」
两者分工明确,且顺序固定:ErrorBoundary 在外、Suspense 在内。① Suspense 处理「等待」——挂起时显示 fallback(或保留旧内容);② ErrorBoundary 处理「错误」——use(promise)的 promise reject 时会抛错,由 ErrorBoundary 接管。如果顺序颠倒(Suspense 在外、ErrorBoundary 在内),ErrorBoundary 的错误 UI 会被外层当成「正在加载的内容」,语义混乱且可能一直显示 fallback。 -
「为什么说『Suspense 不提升性能』?」
因为它不减少工作量——该渲染的还要渲染、该请求的还要请求。它改善的是两件事:① 感知(避免「内容 → 骨架 → 内容」的闪烁,尤其在 transition 下);② 调度粒度(让「慢的部分」不阻塞「快的部分」——服务端可以流式先发 shell(见 9.6),客户端可以「只挂起那一块」而不是整页)。所以它的价值是「让等待更可接受 + 让渲染更可分割」,而不是「让渲染更快」。
一句话速记
Suspense 的唯一协议是「渲染期间
throw thenable」——React 把它当「挂起信号」而不是错误;挂起后 React 会「登记suspendedLanes+ 给 promise 挂 ping 回调」,promise 完成后通过pingedLanes接回调度系统并重试渲染;是否显示 fallback 取决于「这次更新是不是 transition」——transition 中的挂起会保留旧内容(不显示 fallback),非 transition 才显示 fallback(这就是「Tab/路由切换不闪骨架」的正确写法);节流只是「短时间挂起不显示 fallback」的兜底;多个边界默认各自独立揭示,可用SuspenseList(实验)控制forwards/backwards/together;SSR 下总是先输出 fallback(HTML 流必须有序,所以 transition 救不了首屏骨架);ErrorBoundary 在外、Suspense 在内(分别管「错误」与「等待」);它不提升性能,只改善「感知」与「调度粒度」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!