Files
2026-05-24 11:42:38 +08:00

9.1 KiB
Raw Permalink Blame History

tags, create time
tags create time
React
Concurrent
Suspense
Transitions
Frontend
2026-04-29 22:12

并发特性

概述

React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为可中断、可恢复、可优先级调度的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。

[!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死?

答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。

React 渲染架构演进

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 —— 声明式等待

基本用法

// LazyComponent 在首次挂载时自动 code-split
const SettingsPage = lazy(() => import("./pages/SettingsPage"));

<Suspense fallback={<PageSkeleton />}>
  <SettingsPage />
</Suspense>

Suspense + SSR(流式渲染)

[!tip] React 18 的 Streaming SSR

Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。

// 服务端组件中嵌套 Suspense boundary
export default async function Page() {
  return (
    <main>
      <Header /> {/* 同步渲染 */}
      <Suspense fallback={<SearchSkeleton />}>
        <SearchResults /> {/* 等待 fetch 完成后插入 */}
      </Suspense>
    </main>
  );
}

服务端输出变成渐进式 HTML 流:Header → SearchSkeleton → SearchResults(loaded)

Suspense + Data Fetching(实验性 API)

import { use } from "react";

// 资源标记为 Suspense-compatible
const promise = fetchData("/api/profile");

function Profile() {
  // use() 会 suspend 直到 promise resolve
  const profile = use(promise);
  
  return <div>{profile.name}</div>;
}

// 外层用 Suspense 包裹
<Suspense fallback={<ProfileLoading />}>
  <Profile />
</Suspense>

useTransition —— 标记低优先级更新

[!question] 什么时候该用 Transition?

当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<Result[]>([]);
  const [isPending, startTransition] = useTransition();
  
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    
    // 立即响应输入
    setQuery(value);
    
    // 低优先级的结果更新(可被输入打断)
    startTransition(() => {
      setResults(heavySearch(value));
    });
  };
  
  return (
    <>
      <input value={query} onChange={handleInputChange} />
      {isPending && <Spinner />}
      <Results list={results} />
    </>
  );
}

useTransition + Suspense 组合模式

两者配合可以实现更精细的加载策略:Transition 控制哪个更新「可以等待」,Suspense 在数据就绪后展示最终内容。

const [isPending, startTransition] = useTransition();
const deferredTab = useDeferredValue(activeTab);

return (
  <>
    <Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
    {isPending && <PageSkeleton />}
    <Suspense fallback={<SectionLoading />}>
      <ActiveSection tab={deferredTab} />
    </Suspense>
  </>
);

Transition vs DeferredValue 选择指南

场景 推荐 原因
路由切换后展示新视图 useTransition 明确标记整页过渡状态,配合 <Suspense>
输入框即时搜索 + 延迟加载 useDeferredValue 一行搞定,自动处理防抖逻辑
多个状态联动更新 useTransition 更精确控制哪些 setState 属于低优先级
简单延迟一个值(如过滤条件) useDeferredValue 侵入性最小,不改变组件交互模式
Tab 切换时内容区域的异步加载 两者组合 startTransition 控制页面级 Pending,useDeferredValue 控制数据流

[!tip] 核心区别一句话

  • useTransition:控制**「哪个 setState」**是低优先级的 → 你直接包裹需要降级的那段 setState 调用。
  • useDeferredValue:控制**「哪个值」**是延迟更新的 → 你用 const delayed = useDeferredValue(value) 拿到一个滞后 ~16ms 的副本。

useDeferredValue —— 延迟副本

function TodoApp() {
  const [filter, setFilter] = useState("");
  const deferredFilter = useDeferredValue(filter);  // 延迟 ~16ms
  
  return (
    <>
      {/* 快速响应用户输入 */}
      <input value={filter} onChange={e => setFilter(e.target.value)} placeholder="过滤..." />
      
      {/* 耗时操作使用延迟值 */}
      <TodoList todos={allTodos} filter={deferredFilter} />
    </>
  );
}

[!note] 内部机制

useDeferredValue 内部等价于一次 startTransition。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。

时间切片原理

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: 如果用户点击了按钮<br/>高优先级任务插入队列
    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] 需要注意的行为变化

// 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

// ❌ 问题:异步 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<AbortController>();

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. 合理使用 <Suspense> boundary 层级:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。
  4. 用 use() + Suspense 替代 useEffect 中的数据获取:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。
  5. 警惕 useEffect 的异步执行时机:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 useSyncExternalStore 或使用 ref 存储最新值。

关联笔记

  • 11-组件通信模式
  • 12-HOC 与 Render Props
  • 14-Serverside Rendering