说说对 React 并发模式的理解?

一、是什么

并发渲染(Concurrent Rendering)是 React 18 引入的一种新的渲染机制。需要特别澄清的是,React 团队已不再使用"并发模式(Concurrent Mode)"这一术语——并发渲染不是一个开关式的模式,而是一种底层能力,只有在使用了并发特性(如 useTransitionuseDeferredValue)时才会激活。

在传统的同步渲染中,一旦 React 开始渲染一棵组件树,它必须完成所有工作后才能响应用户交互。并发渲染打破了这个限制——React 可以暂停正在进行的渲染工作,去处理更紧急的更新,然后再恢复之前的渲染。

二、同步渲染 vs 并发渲染

2.1 同步渲染的问题

function App() {
  const [query, setQuery] = useState('');
  const [list, setList] = useState([]);

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value);
    // 假设 filterItems 需要遍历 10000 个项目
    setList(filterItems(value));
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <ItemList items={list} />
    </div>
  );
}

在同步渲染下,setQuerysetList 的更新在同一次渲染中处理。如果 filterItemsItemList 渲染耗时较长,用户的输入会出现明显卡顿——因为浏览器在 React 完成渲染前无法响应后续输入。

2.2 并发渲染如何解决

并发渲染下,React 可以:

  1. 中断:低优先级的渲染(如列表更新)可以被高优先级的更新(如输入框响应)中断
  2. 丢弃:如果新的更新到来,React 可以丢弃正在进行但已过时的渲染工作
  3. 恢复:在高优先级任务完成后,恢复之前被中断的渲染

三、时间切片(Time Slicing)

时间切片是并发渲染的实现手段。React 将渲染工作分割成小的工作单元,每完成一个单元就检查是否有更高优先级的任务需要处理:

同步渲染(阻塞):
[========= 渲染整棵树 =========][浏览器绘制]
                                 ↑ 用户无法交互

并发渲染(可中断):
[渲染A][让出][渲染B][让出][渲染C][浏览器绘制][渲染D]...
       ↑     ↑     ↑
    检查是否有更紧急任务

React 使用 MessageChannel(而非 requestIdleCallback)实现任务调度,默认每帧留给 React 约 5ms 的时间片。

四、优先级与 Lanes 模型

React 18 使用 Lanes(车道)模型来管理更新优先级。不同的更新被分配到不同的 Lane,优先级从高到低:

Lane优先级触发方式
SyncLane最高(同步)flushSync、离散事件(click)
InputContinuousLane连续事件(scroll、mousemove)
DefaultLane普通setTimeout、网络回调
TransitionLanestartTransition
IdleLane最低requestIdleCallback
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    // 高优先级:立即更新输入框
    setQuery(e.target.value);

    // 低优先级(TransitionLane):可以被中断
    startTransition(() => {
      setResults(searchItems(e.target.value));
    });
  }

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

在这个例子中,输入框的更新(setQuery)获得高优先级,搜索结果的更新(setResults)获得低优先级。即使搜索结果渲染很慢,输入框依然保持流畅响应。

五、如何启用并发渲染

并发渲染并不需要"开启"——只需使用 createRoot 作为渲染入口,React 就具备了并发渲染的能力:

import { createRoot } from 'react-dom/client';

// createRoot 使应用具备并发渲染能力
const root = createRoot(document.getElementById('root'));
root.render(<App />);

但只有在代码中使用了并发特性时,React 才会实际进行并发渲染:

import { useTransition, useDeferredValue, startTransition } from 'react';

// 方式一:useTransition Hook
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function switchTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <TabBar onSwitch={switchTab} isPending={isPending} />
      <TabContent tab={tab} />
    </div>
  );
}

// 方式二:useDeferredValue
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  return <HeavyList filter={deferredQuery} />;
}

// 方式三:模块级 startTransition(不需要 isPending)
import { startTransition } from 'react';

startTransition(() => {
  setState(newValue);
});

六、与 Suspense 的集成

并发渲染与 Suspense 深度集成。在同步渲染中,Suspense 挂起时会立即显示 fallback;在并发渲染中,React 可以选择继续显示旧内容,而在后台准备新内容:

function App() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function switchTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <TabBar active={tab} onSwitch={switchTab} />
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        <Suspense fallback={<Loading />}>
          {tab === 'home' && <HomePage />}
          {tab === 'posts' && <PostsPage />}
          {tab === 'settings' && <SettingsPage />}
        </Suspense>
      </div>
    </div>
  );
}

当用户切换 Tab 时:

  • 不使用 Transition:立即切到 fallback(Loading),数据就绪后显示内容
  • 使用 Transition:继续显示当前 Tab(略微变暗表示 pending),新 Tab 在后台渲染就绪后再切换

七、并发渲染的心智模型

可以将并发渲染想象为 Git 分支:

主分支(屏幕显示):  A ────────────────── B(紧急更新结果)
                         \              ↗
特性分支(后台渲染):    └── C ── D ──
                              (transition 渲染,可被丢弃或合并)
  • React 在"后台分支"上进行 transition 渲染
  • 渲染过程中不会对屏幕产生任何影响
  • 如果有新的紧急更新,可以中断后台分支,先处理主分支
  • 后台分支完成后,将结果一次性"合并"到屏幕

八、实际影响与注意事项

8.1 渲染可能执行多次

在并发模式下,render 阶段(函数组件的函数体)可能被执行多次然后丢弃。因此必须确保渲染逻辑是纯函数

// 正确:纯渲染逻辑
function List({ items }) {
  const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
  return sorted.map(item => <Item key={item.id} data={item} />);
}

// 错误:渲染阶段有副作用
function List({ items }) {
  analytics.track('list_rendered'); // 可能被调用多次!
  items.sort((a, b) => a.name.localeCompare(b.name)); // 修改了 props!
  return items.map(item => <Item key={item.id} data={item} />);
}

8.2 外部状态的一致性

并发渲染下,同一次渲染过程中读取外部可变状态可能得到不同的值(tearing)。useSyncExternalStore 就是为解决这个问题而设计的:

import { useSyncExternalStore } from 'react';

const store = {
  value: 0,
  listeners: new Set(),
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  },
  getSnapshot() {
    return this.value;
  },
  setValue(v) {
    this.value = v;
    this.listeners.forEach(l => l());
  },
};

function Counter() {
  const value = useSyncExternalStore(
    store.subscribe.bind(store),
    store.getSnapshot,
  );
  return <span>{value}</span>;
}

8.3 Effect 的幂等性

由于 Strict Mode 的双重调用以及并发渲染可能导致的多次挂载,Effect 必须具备幂等性并正确清理:

useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData);
  return () => controller.abort();
}, []);

九、总结

概念说明
并发渲染一种能力而非模式,使渲染可中断
时间切片将渲染拆分为小单元,每帧让出主线程
Lanes优先级调度系统,不同更新获得不同优先级
启用方式createRoot + 并发特性 API
核心价值保持 UI 响应性,紧急更新不被阻塞
开发要求渲染逻辑必须是纯函数

并发渲染是 React 架构的一次根本性演进。它并不会让渲染更快,而是让 React 更聪明地安排渲染工作——确保用户感知到的响应速度始终是最优的。