前置知识: React

Concurrent 模式

4 min高级

并发渲染与Suspense集成

前置知识

学习目标

  • 掌握「概述」的核心机制、典型用法与常见陷阱
  • 掌握「基础概念」的核心机制、典型用法与常见陷阱
  • 掌握「快速上手」的核心机制、典型用法与常见陷阱
  • 掌握「详细用法」的核心机制、典型用法与常见陷阱
  • 掌握「常见场景」的核心机制、典型用法与常见陷阱

概述

Concurrent 模式是 React 18 引入的核心特性,允许 React 在渲染过程中中断、暂停和恢复工作。传统模式下 React 的渲染是同步不可中断的,一旦开始就会执行到底,这可能导致长时间的任务阻塞用户交互。并发渲染通过可中断的渲染机制,使 React 能够优先处理高优先级更新(如用户输入),将低优先级更新(如数据获取)推迟到空闲时执行。

并发模式不是一个新的 API 或模式,而是一组功能的统称,包括 useTransition、useDeferredValue、Suspense 和流式 SSR 等。

基础概念

同步渲染 vs 并发渲染

特性同步渲染并发渲染
渲染方式不可中断,一气呵成可中断、可恢复
优先级所有更新同等优先区分紧急和非紧急更新
用户感知长任务可能导致卡顿高优先级更新立即响应
兼容性React 17 及之前React 18+

优先级模型

React 将更新分为不同优先级,高优先级更新可以中断低优先级渲染:

  • 紧急更新(UserBlocking):用户交互,如输入、点击
  • 普通更新(Normal):数据请求结果
  • 过渡更新(Transition):UI 切换,如标签页切换
  • 空闲更新(Idle):预加载、分析上报

快速上手

useTransition 标记非紧急更新

import { useTransition, useState } from 'react';

function SearchPage() {
  const [isPending, startTransition] = useTransition();
  const [inputValue, setInputValue] = useState('');
  const [searchQuery, setSearchQuery] = useState('');

  function handleChange(e) {
    // 紧急更新:输入框立即响应
    setInputValue(e.target.value);

    // 非紧急更新:搜索结果可以延迟显示
    startTransition(() => {
      setSearchQuery(e.target.value);
    });
  }

  return (
    <div>
      <input value={inputValue} onChange={handleChange} />
      {isPending && <span>搜索中...</span>}
      <SearchResults query={searchQuery} />
    </div>
  );
}

useDeferredValue 延迟更新

import { useDeferredValue, useMemo } from 'react';

function SearchPage({ query }) {
  // 延迟版本的查询值,让紧急更新优先
  const deferredQuery = useDeferredValue(query);

  // 使用延迟值计算结果,避免阻塞输入
  const results = useMemo(() => search(deferredQuery), [deferredQuery]);

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <ResultList results={results} />
    </div>
  );
}

详细用法

Suspense 与并发渲染

import { Suspense } from 'react';

// 数据获取组件,使用 Suspense 等待
function UserProfile({ userId }) {
  const user = useFetchUser(userId); // 抛出 Promise 触发 Suspense
  return <div>{user.name}</div>;
}

// 使用 Suspense 包裹
function App() {
  return (
    <div>
      <h1>用户中心</h1>
      <Suspense fallback={<Loading />}>
        <UserProfile userId={1} />
      </Suspense>
    </div>
  );
}

Suspense 与多数据源

import { Suspense } from 'react';

function Dashboard() {
  return (
    <div className="dashboard">
      {/* 每个区域独立加载,互不影响 */}
      <section>
        <Suspense fallback={<Skeleton />}>
          <UserProfile />
        </Suspense>
      </section>

      <section>
        <Suspense fallback={<ChartSkeleton />}>
          <AnalyticsChart />
        </Suspense>
      </section>

      <section>
        <Suspense fallback={<ListSkeleton />}>
          <RecentActivities />
        </Suspense>
      </section>
    </div>
  );
}

useTransition 与列表过滤

import { useTransition, useState } from 'react';

function FilterableList({ items }) {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');

  const filteredItems = useMemo(() => {
    return items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()));
  }, [items, filter]);

  function handleFilterChange(e) {
    // 输入框立即响应
    const value = e.target.value;

    // 过滤操作标记为过渡更新
    startTransition(() => {
      setFilter(value);
    });
  }

  return (
    <div>
      <input onChange={handleFilterChange} placeholder="搜索..." />
      <ul style={{ opacity: isPending ? 0.7 : 1 }}>
        {filteredItems.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

常见场景

标签页切换

import { useTransition, useState } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [activeTab, setActiveTab] = useState('overview');

  function switchTab(tab) {
    // 标签切换标记为过渡更新
    startTransition(() => {
      setActiveTab(tab);
    });
  }

  return (
    <div>
      <nav>
        <button onClick={() => switchTab('overview')}>概览</button>
        <button onClick={() => switchTab('details')}>详情</button>
        <button onClick={() => switchTab('settings')}>设置</button>
      </nav>
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        {activeTab === 'overview' && <OverviewTab />}
        {activeTab === 'details' && <DetailsTab />}
        {activeTab === 'settings' && <SettingsTab />}
      </div>
    </div>
  );
}

流式 SSR

// 服务端:使用 renderToPipeableStream 实现流式渲染
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const stream = renderToPipeableStream(<App />, {
    onShellReady() {
      // HTML 骨架就绪,开始流式传输
      res.setHeader('content-type', 'text/html');
      stream.pipe(res);
    },
    onShellError(error) {
      // 骨架渲染失败
      res.status(500).send('服务端渲染失败');
    },
    onError(error) {
      console.error(error);
    },
  });
});

注意事项

  • useTransition 和 useDeferredValue 不能用于受控输入的值,输入框的值必须同步更新
  • isPending 为 true 时不要隐藏或卸载旧内容,应使用透明度等视觉提示
  • Suspense 的 fallback 不应过于复杂,否则会增加首屏渲染时间
  • 并发特性不会改变代码的执行结果,只改变渲染的时机和优先级
  • React 18 默认启用了并发特性,不再需要 ConcurrentMode 包裹
  • startTransition 中的状态更新不能用于紧急的副作用(如路由跳转)

进阶用法

Suspense 与错误边界结合

import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function SafeDataComponent({ userId }) {
  return (
    <ErrorBoundary
      fallback={<div>数据加载失败,请重试</div>}
      onReset={() => {
        /* 重置逻辑 */
      }}
    >
      <Suspense fallback={<Loading />}>
        <UserProfile userId={userId} />
      </Suspense>
    </ErrorBoundary>
  );
}

Selective Hydration

// React 18 的选择性水合:Suspense 边界内的组件不会阻塞其他组件的水合
function Page() {
  return (
    <Layout>
      {/* 这部分立即水合 */}
      <NavBar />

      {/* 这部分可以延迟水合 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>

      {/* 这部分也立即水合 */}
      <Footer />
    </Layout>
  );
}

useTransition 与乐观更新

import { useTransition, useState } from 'react';

function LikeButton({ postId, initialLiked }) {
  const [isPending, startTransition] = useTransition();
  const [liked, setLiked] = useState(initialLiked);

  function handleLike() {
    // 乐观更新:立即反映用户操作
    startTransition(async () => {
      setLiked(!liked);
      try {
        await toggleLike(postId);
      } catch {
        // 失败时回滚
        setLiked(liked);
      }
    });
  }

  return (
    <button onClick={handleLike} disabled={isPending}>
      {liked ? '已点赞' : '点赞'}
    </button>
  );
}

useTransition 过渡更新

基本写法:将状态更新标记为低优先级 const [<isPending>, <startTransition>] = useTransition()

// 切换标签页保持输入框响应
const [isPending, startTransition] = useTransition();
function changeTab(next) {
  startTransition(() => setTab(next));
}

基本写法:展示过渡中状态 {<isPending> && <占位>}

// 显示加载指示
{isPending ? <Spinner /> : <Content />}

基本写法:异步 Action startTransition(async () => { <异步逻辑> })

// React 19 支持异步过渡
startTransition(async () => {
  const data = await fetchData();
  setResult(data);
});

startTransition 全局函数

基本写法:在组件外标记过渡更新 startTransition(() => <更新>)

// 从非组件代码触发过渡
import { startTransition } from 'react';
startTransition(() => store.setFilter('active'));

useDeferredValue 延迟值

基本写法:延迟非紧急值的更新 const <延迟值> = useDeferredValue(<值>)

// 搜索框输入即时响应结果延迟
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);

基本写法:检测是否处于滞后状态 const <是否滞后> = <值> !== <延迟值>

// 显示旧数据淡化效果
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.7 : 1 }}>{results}</div>

Suspense 数据等待

基本写法:包裹异步组件显示降级 <Suspense fallback={<占位>}> <异步组件 /> </Suspense>

// 数据未就绪时显示骨架
<Suspense fallback={<Skeleton />}>
  <Profile userId={id} />
</Suspense>

基本写法:嵌套 Suspense 边界 <Suspense fallback={<外层>}> <Suspense fallback={<内层>}> <组件/> </Suspense> </Suspense>

// 不同区域独立 loading
<Suspense fallback={<PageFallback />}>
  <Header />
  <Suspense fallback={<ListFallback />}>
    <List />
  </Suspense>
</Suspense>

Suspense 配合 lazy

基本写法:路由级代码分割 const <组件> = lazy(() => import(<路径>))

// 按需加载并显示 fallback
const Settings = lazy(() => import('./Settings'));
<Suspense fallback={<Spinner />}><Settings /></Suspense>

并发更新优先级

基本写法:紧急更新直接 setState <设置>(<值>)

// 输入框立即响应属于高优先级
setInput(e.target.value);

基本写法:非紧急更新放入 transition startTransition(() => <设置>(<值>))

// 搜索结果可延迟
startTransition(() => setResults(filtered));

避免不必要的 loading

基本写法:使用 useTransition 避免跳到 fallback startTransition(() => <切换>)

// 切换 tab 时保留当前内容直到新内容就绪
startTransition(() => setTab(next));

并发渲染可中断

基本写法:渲染过程可被打断让位高优先级 startTransition(() => <更新>)

// 用户输入打断后台渲染
function onType(v) {
  setInput(v); // 紧急
  startTransition(() => setMatches(filter(v))); // 可中断
}

useSyncExternalStore 订阅外部

基本写法:安全订阅外部 store 避免 tearing const <快照> = useSyncExternalStore(<订阅>, <取值>, [<服务端取值>])

// 订阅 window 尺寸
const width = useSyncExternalStore(
  cb => window.addEventListener('resize', cb),
  () => window.innerWidth
);

基本写法:提供 getServerSnapshot 支持 SSR useSyncExternalStore(<订阅>, <取值>, <服务端取值>)

// 服务端返回默认值
const theme = useSyncExternalStore(subscribe, getSnapshot, () => 'light');

自动批处理

基本写法:同一事件多次更新合并 <设置1>(<值1>); <设置2>(<值2>);

// React 18+ 自动合并为一次渲染
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
}

基本写法:异步代码也自动批处理 await <异步>; <设置>(<值>);

// Promise 内的更新也会合并
async function load() {
  const data = await fetch();
  setList(data);
  setLoading(false);
}

flushSync 强制同步

基本写法:跳过批处理立即刷新 flushSync(() => <更新>)

// 需要立即读取 DOM 时使用
import { flushSync } from 'react-dom';
flushSync(() => setScroll(0));
window.scrollTo(0, 0);

selectNode 调度提示

基本写法:useDeferredValue 实现非阻塞渲染 const <延迟> = useDeferredValue(<值>)

// 大列表过滤不阻塞输入
const deferred = useDeferredValue(text);
const items = useMemo(() => heavyFilter(deferred), [deferred]);

Activity 隐藏组件

基本写法:保留组件状态但隐藏显示 <Activity mode="hidden"> <组件 /> </Activity>

// React 19.2 正式 API(由实验性 Offscreen 更名而来)
// 隐藏时卸载 Effect、保留 state,适合标签页切换与预渲染
<Activity mode={visible ? 'visible' : 'hidden'}>
  <ExpensiveList />
</Activity>

useOptimistic 乐观更新

基本写法:在请求期间乐观展示结果 const [<乐观值>, <添加>] = useOptimistic(<状态>, <更新函数>)

// 立即显示新消息
const [messages, addOptimistic] = useOptimistic(messages, (state, newMsg) => [...state, newMsg]);

Suspense List 协调多个 Suspense

基本写法:控制多个 Suspense 揭示顺序 <SuspenseList revealOrder="forwards"> <Suspense>...</Suspense> </SuspenseList>

// 按顺序揭示内容(实验性 API)
<SuspenseList revealOrder="forwards">
  <Suspense fallback={<S1 />}><A /></Suspense>
  <Suspense fallback={<S2 />}><B /></Suspense>
</SuspenseList>

React 19 Actions

基本写法:在 startTransition 中执行异步函数即 Action startTransition(async () => <异步>)

// Actions 自动管理 pending 与错误
const [isPending, startTransition] = useTransition();
startTransition(async () => await submitForm(data));

并发模式注意事项

基本写法:transition 内不可包含读取状态副作用 startTransition(() => { <设置>; })

// 仅做状态更新不读取
startTransition(() => setTab(next));

transition 与 Suspense 配合

基本写法:transition 内挂起会显示当前 UI 而非 fallback startTransition(() => <切换挂起组件>)

// 切换路由不闪烁 loading
startTransition(() => setRoute('/detail'));

性能权衡

基本写法:仅对昂贵非紧急更新使用 transition startTransition(() => <昂贵更新>)

// 简单更新无需 transition 开销
setCount(c => c + 1);