Suspense 与加载体验
Suspense 与加载体验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完你能解释 Suspense 边界的工作方式:边界内的子组件在数据未就绪时抛出 Promise,最近的边界接管并显示 fallback;能按用户的等待粒度合理放置边界,让加载只发生在局部,并用 startTransition 避免切换时的整页闪烁。
问题场景
课程详情页:顶部标题、中间章节列表、右侧相关推荐。第一版把加载逻辑写成「数据没到就整页返回加载动画」,于是切个数据源,整页白一下、骨架屏闪一下,用户刚看到的内容全部消失。加载体验差,往往不是没有 loading,而是 loading 的边界放错了位置。
核心解释
Suspense 的机制一句话:子组件在渲染中抛出一个 Promise(通常由数据层封装),React 捕获后找到最近的 <Suspense> 显示 fallback;Promise 决议后自动重试渲染并展示真实内容。
<Suspense fallback={<ChaptersSkeleton />}>
<Chapters id={id} /> {/* 抛出 Promise 时,只有这里被 fallback 替换 */}
</Suspense>
边界放置有两条经验。第一,fallback 的粒度 = 用户的等待粒度:用户切一个数据源时期待的是「对应区域内容刷新」,整页 fallback 就过重;首屏期待的是「页面骨架」,单个小卡片的 fallback 又太碎。第二,已经被看到的内容不要因为别处的加载而消失——导航类更新包进 startTransition,新数据未就绪时 React 会保留旧界面:
function onSelect(nextId) {
startTransition(() => setSourceId(nextId)); // 旧内容保留到新内容就绪
}
什么情况用:组件树里有异步数据或懒加载代码、希望「声明式地」描述加载态时,用 Suspense 组织;如果只是「请求期间显示 spinner」,局部 state 加条件渲染一样能做,不必强行套 Suspense。验证方式:在实验区把边界从「整页一个」改成「每个面板各一个」,切换数据时对比:前者整页闪骨架,后者只有对应面板变化。
示例拆解
实验区模拟课程详情页的两个面板(章节列表 / 相关推荐),数据各有 900ms 延迟:
- 「局部边界」开关:开 = 每个面板各自包 Suspense;关 = 整页一个 Suspense;
- 「刷新两个面板的数据」按钮触发新一轮请求;整页模式下任一面板加载都会整页骨架闪现;
- 「切换包进 startTransition」开关:开启后切换时旧内容保留,按钮上出现「加载中(旧内容保留)」,新内容就绪后一并切换;
- 验证:三种配置各切换两次,对比闪烁范围、内容保留情况和 isPending 提示。
动手任务
打开本课实验区:
- 关掉「局部边界」刷新两次,记录整页闪烁的体验;再打开局部边界对比,说出差别。
- 给面板容器固定最小高度(现在是 90px),让局部加载的骨架不引起布局跳动。
- 打开 startTransition 后连续快速切换两次,确认最终显示的是最后一次数据——说明被打断的过渡不会留下中间结果。
常见错误
- 边界放得太高:整页一个 Suspense,任何局部加载都整页闪烁;fallback 粒度要跟随用户的等待粒度。
- 抛出 Promise 却没有边界接:组件在渲染中抛出 Promise 而上层没有 Suspense,React 直接报错;抛出必须配对边界。
- 把 Suspense 当错误处理:它只管「未就绪」,请求失败抛出的是错误,要靠下一课的错误边界接管。
- fallback 与真实内容尺寸差太多:加载完成瞬间布局大跳;给 fallback 预留接近真实内容的占位高度。
检查与复盘
自查清单:
- 能完整复述机制:抛 Promise → 最近边界显示 fallback → 就绪后重试渲染
- 边界粒度经过设计,页面不会因为局部加载而整页闪
- 导航类切换包了 startTransition,旧内容保留、验证过不闪
参考资料
- React 官方文档:Suspense —— 边界语义、fallback 与嵌套规则
- React 官方文档:use —— 在渲染中读取 Promise 的推荐方式,与 Suspense 配合
读完这一课了?
标记完成后,工作台会安排接下来的学习。