--- tags: [React, Concurrent, Suspense, Transitions, Frontend] create time: 2026-04-29 22:12 --- # 并发特性 ## 概述 React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为**可中断、可恢复、可优先级调度**的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。 > [!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死? > > 答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。 ## React 渲染架构演进 ```mermaid graph LR A["React 17 同步渲染"] -->|"全部一次性完成"| B["长时间阻塞主线程 ❌"] C["React 18 并发渲染"] -->|"可打断/可恢复"| D["Fiber Scheduler"] D --> E["高优先级任务:用户输入 ⏩"] D --> F["低优先级任务:数据加载 🐢"] F -->|"被高优先级打断"| G["暂停 → 之后恢复 ✅"] style B fill:#F5A87D,color:#000 style G fill:#4FC08D,color:#fff ``` ## Suspense —— 声明式等待 ### 基本用法 ```tsx // LazyComponent 在首次挂载时自动 code-split const SettingsPage = lazy(() => import("./pages/SettingsPage")); }> ``` ### Suspense + SSR(流式渲染) > [!tip] React 18 的 Streaming SSR > > Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。 ```tsx // 服务端组件中嵌套 Suspense boundary export default async function Page() { return (
{/* 同步渲染 */} }> {/* 等待 fetch 完成后插入 */}
); } ``` 服务端输出变成**渐进式 HTML 流**:`Header → SearchSkeleton → SearchResults(loaded)` ### Suspense + Data Fetching(实验性 API) ```tsx import { use } from "react"; // 资源标记为 Suspense-compatible const promise = fetchData("/api/profile"); function Profile() { // use() 会 suspend 直到 promise resolve const profile = use(promise); return
{profile.name}
; } // 外层用 Suspense 包裹 }> ``` ## useTransition —— 标记低优先级更新 > [!question] 什么时候该用 Transition? > > 当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。 ```tsx function SearchPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; // 立即响应输入 setQuery(value); // 低优先级的结果更新(可被输入打断) startTransition(() => { setResults(heavySearch(value)); }); }; return ( <> {isPending && } ); } ``` ### useTransition + Suspense 组合模式 两者配合可以实现更精细的加载策略:Transition 控制哪个更新「可以等待」,Suspense 在数据就绪后展示最终内容。 ```tsx const [isPending, startTransition] = useTransition(); const deferredTab = useDeferredValue(activeTab); return ( <> {isPending && } }> ); ``` ### Transition vs DeferredValue 选择指南 | 场景 | 推荐 | 原因 | |------|------|------| | 路由切换后展示新视图 | `useTransition` | 明确标记整页过渡状态,配合 `` | | 输入框即时搜索 + 延迟加载 | `useDeferredValue` | 一行搞定,自动处理防抖逻辑 | | 多个状态联动更新 | `useTransition` | 更精确控制哪些 setState 属于低优先级 | | 简单延迟一个值(如过滤条件) | `useDeferredValue` | 侵入性最小,不改变组件交互模式 | | Tab 切换时内容区域的异步加载 | 两者组合 | `startTransition` 控制页面级 Pending,`useDeferredValue` 控制数据流 | > [!tip] 核心区别一句话 > > - **`useTransition`**:控制**「哪个 setState」**是低优先级的 → 你直接包裹需要降级的那段 setState 调用。 > - **`useDeferredValue`**:控制**「哪个值」**是延迟更新的 → 你用 `const delayed = useDeferredValue(value)` 拿到一个滞后 ~16ms 的副本。 ## useDeferredValue —— 延迟副本 ```tsx function TodoApp() { const [filter, setFilter] = useState(""); const deferredFilter = useDeferredValue(filter); // 延迟 ~16ms return ( <> {/* 快速响应用户输入 */} setFilter(e.target.value)} placeholder="过滤..." /> {/* 耗时操作使用延迟值 */} ); } ``` > [!note] 内部机制 > > `useDeferredValue` 内部等价于一次 `startTransition`。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。 ## 时间切片原理 ```mermaid sequenceDiagram participant Browser as 浏览器主线程 participant Fiber as Fiber Scheduler Browser->>Fiber: 构建整棵组件树(工作单元) Fiber->>Browser: 渲染帧 1(约 16ms)✅ Note over Fiber,Browser: 把任务切分为 ~5ms 的工作单元 Browser->>Fiber: 下一帧开始 Fiber->>Browser: 渲染帧 2 ✅ Note over Fiber: 如果用户点击了按钮
高优先级任务插入队列 Fiber-->>Browser: 暂停当前帧 Browser->>Fiber: 处理用户输入 ✅ Fiber->>Browser: 继续未完成的渲染帧 style Browser fill:#F5A87D,color:#000 style Fiber fill:#4FC08D,color:#fff ``` ### 关键概念 - **Work Breakdown**:React 将渲染工作拆成多个小单元(work unit),每个单元约 1ms - **Yield to Browser**:每个单元完成后让出控制权给浏览器,确保 UI 响应性 - **Priority Scheduling**:用户输入 > 网络响应 > 后台数据更新 > 动画 ## 并发模式下的陷阱 > [!warning] 需要注意的行为变化 ```tsx // 1. useEffect 的执行时机变了 useEffect(() => { console.log("effect"); }, [dep]); // 效果不再与渲染同步,可能在异步 commit 阶段执行 // 2. 浏览器生命周期方法(如 getSnapshotBeforeUpdate)已过时 // 并发模式下无法保证同步 commit // 3. 旧版第三方库可能与并发不兼容 // 遇到 "Cannot update a component during rendering" → 检查是否有不规范的 side effect // 4. StrictMode 双重渲染(开发环境) // 用于发现不纯的 render 函数和清理逻辑缺失 ``` ## 最佳实践 > [!tip] 实战经验总结 ### 代码示例:安全的 Effect Cleanup ```tsx // ❌ 问题:异步 render → effect 执行时 dep 可能已变化 useEffect(() => { const controller = new AbortController(); fetchData(url, { signal: controller.signal }).then(setData); return () => controller.abort(); // cleanup 依赖的 url 可能已过时 }, [dep]); // ✅ 推荐:用 ref 缓存最新值,保证 cleanup 拿到正确的信号 const latestSignalRef = useRef(); useEffect(() => { const controller = new AbortController(); latestSignalRef.current = controller; fetchData(url, { signal: controller.signal }).then(setData); return () => { if (latestSignalRef.current === controller) { controller.abort(); // 确认是同一个请求才 abort } }; }, [dep]); ``` ### 要点清单 1. **优先使用 Suspense,谨慎手动 startTransition**:Suspense 配合数据获取模式是 React 官方推荐的方向,而 `startTransition` 适合「一个交互触发多个 setState」的场景。 2. **避免在 render 中做副作用**:并发模式下 render 函数可能被多次调用、随时中断——render 应该是纯粹的「视图描述函数」。 3. **合理使用 `` boundary 层级**:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。 4. **用 `use()` + Suspense 替代 useEffect 中的数据获取**:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。 5. **警惕 `useEffect` 的异步执行时机**:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 `useSyncExternalStore` 或使用 ref 存储最新值。 ## 关联笔记 - [[11-组件通信模式]] - [[12-HOC 与 Render Props]] - [[14-Serverside Rendering]]