「React 18 并发范式」一文吃透:从同步 UI 到可中断渲染的十倍飞跃

42 阅读7分钟

关注新特性,渐进式拥抱新特性,可不敢一次直接冲啊!

0. 核心思想速读

  • 并发渲染(Concurrent Rendering) :让渲染可中断、可恢复、可批处理,以更平滑的交互取代“卡一下再显示”的体验。
  • 优先级调度:React 会给不同更新赋予不同优先级,高优先(输入、动画)可打断低优先(数据渲染)
  • 可控的“重要/不重要”更新:通过 startTransition / useTransition / useDeferredValue不影响即时交互的更新“降级”,让 UI 不卡顿。
  • 更激进的批处理自动批处理跨事件源(微任务/宏任务/原生事件)的多次 setState,减少无谓渲染。
  • SSR 全链路升级:支持流式 SSR(Streaming)选择性水合(Selective Hydration) ,慢数据不阻塞快数据。

1. 并发渲染入口:createRoot(取代 ReactDOM.render

做什么:启用并发渲染能力的入口。
为何重要:只有 createRoot 才能使用 React 18 的并发调度、自动批处理、Transitions 等完整能力。

// index.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const rootEl = document.getElementById("root")!;
createRoot(rootEl).render(<App />); // ✅ 并发能力开启

友情建议:迁移时先无改动切换到 createRoot,观察告警与行为差异,再逐步引入并发特性。


2. 自动批处理(Automatic Batching)

做什么:在更多场景下把多个 setState 合并为一次渲染。
收益:减少重复渲染,提升吞吐量。
变化点:React 18 前仅在 React 事件中批处理;React 18 起默认跨事件源批处理(例如 setTimeout、Promise then)。

function DemoAutoBatching() {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(0);

  function handleClick() {
    setTimeout(() => {
      setA((x) => x + 1);
      setB((x) => x + 1);
      // React 18:此处也会自动批处理 -> 只触发一次渲染
    }, 0);
  }

  return (
    <div>
      <button onClick={handleClick}>Update A & B</button>
      <p>{a} - {b}</p>
    </div>
  );
}

易错点:极少数场景你需要立即同步刷新(比如读取 DOM 布局或第三方同步计算),请用 flushSync 包裹。

import { flushSync } from "react-dom";

flushSync(() => setCount((c) => c + 1));
// 此回调内更新会同步生效,避免布局/测量错位

3. 让重活“不挡路”:startTransition / useTransition

做什么:把不紧急的更新标记为“过渡”(Transition),允许被更高优先级任务打断。
典型场景:搜索输入、筛选列表、切 Tab、路由切换——输入即时响应,重算/重渲染后台慢慢做。

3.1 命令式:startTransition

import { startTransition, useState } from "react";

function SearchBox({ items }: { items: string[] }) {
  const [query, setQuery] = useState("");
  const [result, setResult] = useState<string[]>([]);

  function onInput(e: React.ChangeEvent<HTMLInputElement>) {
    const q = e.target.value;
    setQuery(q); // 紧急更新:输入框必须“秒回显”
    startTransition(() => {
      // 非紧急:重算过滤结果可被打断
      const filtered = items.filter((x) => x.includes(q));
      setResult(filtered);
    });
  }

  return (
    <>
      <input value={query} onChange={onInput} placeholder="Type to search..." />
      <ul>{result.map((r) => <li key={r}>{r}</li>)}</ul>
    </>
  );
}

3.2 声明式:useTransition

import { useState, useTransition } from "react";

function BigList({ data }: { data: number[] }) {
  const [filter, setFilter] = useState("");
  const [list, setList] = useState<number[]>(data);
  const [isPending, startTransition] = useTransition();

  function onFilterChange(e: React.ChangeEvent<HTMLInputElement>) {
    const v = e.target.value;
    setFilter(v);
    startTransition(() => {
      // 慢更新:允许中断
      setList(data.filter((x) => String(x).includes(v)));
    });
  }

  return (
    <>
      <input value={filter} onChange={onFilterChange} />
      {isPending && <span>筛选中…</span>}
      <ul>{list.map((n) => <li key={n}>{n}</li>)}</ul>
    </>
  );
}

友情建议:只将“可等待”的 UI 放进 Transition。输入光标、按钮点击反馈等必须即时响应的不要降级。


4. “推迟昂贵依赖”:useDeferredValue

做什么:让某个计算昂贵或渲染昂贵的值延后同步,保留旧值以保证 UI 顺滑。
相比 useTransitionuseTransition 控制“触发时机”;useDeferredValue 控制“消费时机”。

import { useDeferredValue, useMemo, useState } from "react";

function DeferredList({ rawItems }: { rawItems: string[] }) {
  const [q, setQ] = useState("");
  const deferredQ = useDeferredValue(q); // 让昂贵计算“吃旧值”,先不卡输入

  const filtered = useMemo(() => {
    // 假设这是昂贵计算
    for (let i = 0; i < 2e6; i++) {}
    return rawItems.filter((x) => x.includes(deferredQ));
  }, [rawItems, deferredQ]);

  return (
    <>
      <input value={q} onChange={(e) => setQ(e.target.value)} />
      <p>立即显示的关键词:{q}</p>
      <p>稍后用于计算的关键词:{deferredQ}</p>
      <ul>{filtered.map((x) => <li key={x}>{x}</li>)}</ul>
    </>
  );
}

实战定位:useDeferredValue 更像渲染层面的节流,适合昂贵子树(图表、虚拟表格、语法高亮)而不适合必须实时同步的区域。


5. Suspense 与并发:让“等待”变优雅(含 SSR)

做什么:在组件粒度上为“加载中”提供占位,辅以并发能力实现选择性水合流式渲染
客户端常见用法:配合代码分割数据请求框架(如 React Router、Next.js、TanStack Query)用。

const UserPanel = React.lazy(() => import("./UserPanel"));

export default function App() {
  return (
    <React.Suspense fallback={<div>加载用户面板…</div>}>
      <UserPanel />
    </React.Suspense>
  );
}

SSR 升级点(概念)

  • Streaming SSR:服务端按块输出 HTML,快的先到、慢的后到,首屏更快。
  • Selective Hydration:某些组件先水合、某些组件后水合,不必“一起堵车”。

友情建议:在客户端控制好 Suspense 边界大小(过大边界会放大“骨架屏”区域),在 SSR 侧用框架内置支持(Next.js/Remix)获得“流式 + 选择性水合”。


6. 新 Hook 与新契约

6.1 useId:无冲突、SSR 友好的稳定 ID

场景aria-* 关联、表单 label/input、SSR 与 CSR 需要一致 ID。

function Field({ label }: { label: string }) {
  const id = React.useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

注意:不要把 useId 结果持久化到业务数据(它只在渲染层稳定,不保证跨进程/存储稳定)。

6.2 useSyncExternalStore:外部状态的一致读写协议

动机:确保并发渲染下对外部 store(Redux、Zustand、自研)一致快照可订阅

// 极简自研 store
const store = (() => {
  let value = 0;
  let subs = new Set<() => void>();
  return {
    getSnapshot: () => value,
    subscribe: (cb: () => void) => (subs.add(cb), () => subs.delete(cb)),
    set: (v: number) => { value = v; subs.forEach((s) => s()); },
  };
})();

function Counter() {
  const v = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
  return (
    <>
      <div>value: {v}</div>
      <button onClick={() => store.set(v + 1)}>+1</button>
    </>
  );
}

友情建议:库作者请用它封装对外部状态的订阅;应用层若使用成熟状态库,升级到支持 useSyncExternalStore 的版本即可。

6.3 useInsertionEffect:样式注入时机(CSS-in-JS 场景)

  • 在 DOM 变更前运行,保证样式优先插入,避免 FOUC(闪烁)。
  • 仅限样式系统/库作者;业务代码尽量避免使用。
function StyleInjector({ css }: { css: string }) {
  React.useInsertionEffect(() => {
    const style = document.createElement("style");
    style.textContent = css;
    document.head.appendChild(style);
    return () => style.remove();
  }, [css]);

  return null;
}

7. 并发友好型性能策略(把特性用“对位”了)

  1. 输入优先:输入框/拖拽/滚动等即时交互保持非过渡更新;数据过滤/渲染改用 startTransition
  2. 昂贵子树隔离:对图表/虚拟列表等耗时组件,使用 useDeferredValue 推迟依赖或在父级 Suspense 包裹骨架。
  3. 避免可中断副作用:数据拉取/副作用不要依赖可能被中断的渲染快照,放入事件或 effect,并做好幂等与取消。
  4. 监控优先渲染路径:以“输入 → 响应”链路为第一优先级优化目标,评估 FPS、TTI、INP(交互到下一绘制)。
  5. SSR 边界划分:在可控粒度上设计 Suspense 边界,对慢资源单独边界,减少“整页骨架”。

8. 迁移与稳定性(真实工程中的“坑位图”)

  • StrictMode(开发环境) :某些 effect 会执行两次(意在发现不安全副作用)。生产不会;但你要保证副作用可重入/可取消。
  • 自动批处理行为改变:老代码里对“每次 setState 都渲染一次”的隐性假设要移除;实在需要同步渲染时用 flushSync
  • 第三方库升级:确保状态管理库(Redux、Zustand)、样式库(styled-components、emotion)已适配 useSyncExternalStore / useInsertionEffect
  • SSR 框架版本:使用 Next.js/Remix 等的 React 18 适配版本,开启 streaming/hydration 才能吃满红利。

9. 综合示例:丝滑搜索面板(输入秒回显 + 重渲染可打断 + 昂贵列表延后)

import React from "react";
import { createRoot } from "react-dom/client";

type Item = { id: number; title: string; body: string };

function useFakeData(size = 50_000): Item[] {
  return React.useMemo(
    () => Array.from({ length: size }, (_, i) => ({
      id: i, title: `Item ${i}`, body: "x".repeat(200)
    })),
    [size]
  );
}

function App() {
  const data = useFakeData();
  const [q, setQ] = React.useState("");
  const deferredQ = React.useDeferredValue(q); // 昂贵列表吃“旧值”
  const [filtered, setFiltered] = React.useState<Item[]>(data);
  const [isPending, startTransition] = React.useTransition();

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const v = e.target.value;
    setQ(v); // 紧急:输入“秒回显”
    startTransition(() => {
      // 非紧急:允许被打断的重算
      const res = data.filter((it) =>
        it.title.includes(v) || it.body.includes(v)
      );
      setFiltered(res);
    });
  };

  const expensiveList = React.useMemo(() => {
    // 模拟昂贵渲染:巨量节点 + 计算
    const heavy = [];
    for (let i = 0; i < 3e6; i++) {} // burn CPU
    for (const it of filtered) heavy.push(<li key={it.id}>{it.title}</li>);
    return heavy;
  }, [filtered, deferredQ]); // 注意:使用 deferredQ 可在某些场景下进一步缓和波动

  return (
    <div>
      <h3>丝滑搜索</h3>
      <input value={q} onChange={onChange} placeholder="键入关键字…" />
      {isPending && <span style={{ marginLeft: 8 }}>计算中…</span>}
      <ul>{expensiveList}</ul>
    </div>
  );
}

createRoot(document.getElementById("root")!).render(<App />);

为何顺滑?

  • 输入框用紧急更新,立刻回显;
  • 重计算放入 Transition,可被后续输入打断;
  • 昂贵渲染依赖 deferredQ消费延后进一步平滑。

10. 学习与落地清单(从“会用”到“用得对”)

  • 全量切换到 createRoot,清理 ReactDOM.render
  • 在“输入→列表”路径引入 startTransition
  • 对图表/虚拟表格引入 useDeferredValue
  • Store 订阅层升级到 useSyncExternalStore
  • CSS-in-JS 升级到支持 useInsertionEffect
  • 客户端拆 Suspense 边界;SSR 打开 Streaming + 选择性水合
  • 建立 INP/FPS 监控面板,度量“交互 → 下一帧”质量
  • 严格检查副作用可重入性(适配 StrictMode)

总结

React 18 的“并发”不是“并行”,而是“可中断、可恢复、可分级”的渲染范式。掌握 startTransition / useDeferredValue / Suspense + Streaming正确边界,再配合自动批处理与外部状态协议,你在复杂/大规模前端里的体验优化与稳定性治理会直接上一个台阶。

展示地址 github.com/huanhunmao/…