React 18 新特性与并发渲染

React 18 的核心更新:自动批处理、Transitions、Suspense 改进和并发渲染机制

问题

React 18 带来了哪些新特性?并发渲染是如何实现的?

解答

自动批处理(Automatic Batching)

React 18 之前,只有 React 事件处理函数内的状态更新会被批处理。React 18 扩展了批处理的范围,Promise、setTimeout、原生事件处理函数中的更新也会自动合并:

// React 18 之前:不会批处理
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 会触发两次渲染
}, 1000);

// React 18:自动批处理
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只触发一次渲染
}, 1000);

如果需要退出自动批处理,可以使用 flushSync

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // DOM 已更新
  
  flushSync(() => {
    setFlag(f => !f);
  });
  // DOM 再次更新
}

Transitions

Transitions 用于区分紧急更新和非紧急更新。紧急更新包括用户输入、点击等需要立即响应的操作;非紧急更新如 UI 过渡动画等可以稍后处理。

使用 startTransition

import { startTransition } from 'react';

function handleChange(input) {
  // 紧急:更新输入框
  setInputValue(input);
  
  // 非紧急:更新搜索结果
  startTransition(() => {
    setSearchResults(input);
  });
}

使用 useTransition

import { useTransition } from 'react';

function SearchPage() {
  const [isPending, startTransition] = useTransition();
  
  function handleChange(input) {
    setInputValue(input);
    startTransition(() => {
      setSearchResults(input);
    });
  }
  
  return (
    <>
      <input onChange={e => handleChange(e.target.value)} />
      {isPending && <Spinner />}
      <Results data={searchResults} />
    </>
  );
}

Suspense 改进

React 18 中的 Suspense 基于并发渲染实现,行为有两个重要变化:

1. 兄弟组件的挂载时机

<Suspense fallback={<Loading />}>
  <ComponentThatSuspends />
  <Sibling />
</Suspense>

React 18 之前,Sibling 会立即挂载到 DOM 并触发 effects,然后被隐藏。React 18 中,Sibling 不会挂载,直到 ComponentThatSuspends 完成加载。

2. ref 的指向时机

<Suspense fallback={<Loading />}>
  <div ref={refPassedFromParent}>
    <ComponentThatSuspends />
  </div>
</Suspense>

React 18 中,refPassedFromParent.current 在 Suspense 解除锁定前保持为 null,而不是立即指向 DOM 节点。

SSR 中的 Suspense

React 18 的 Suspense 支持流式 SSR:

  • 服务器可以先发送 HTML,被 Suspense 包裹的组件用 fallback 占位
  • 组件数据准备好后,通过同一个流发送剩余 HTML
  • 客户端可以逐步进行 hydration,不需要等待所有 JS 加载完成
  • React 会优先对用户交互的区域进行 hydration

新的 API

客户端渲染

// React 18 之前
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// React 18
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// hydration
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

服务端渲染

  • renderToPipeableStream:用于 Node 环境
  • renderToReadableStream:用于 Deno、Cloudflare Workers 等现代运行时

新的 Hooks

useTransition

见上文 Transitions 部分。

useDeferredValue

用于标记低优先级的变量:

import { useDeferredValue } from 'react';

function SearchPage({ input }) {
  const deferredInput = useDeferredValue(input);
  
  // input 变化时,deferredInput 会先返回旧值
  // 如果没有更紧急的更新,才会更新为新值
  return <Results query={deferredInput} />;
}

其他 Hooks

  • useId:生成服务端和客户端一致的唯一 ID
  • useSyncExternalStore:用于第三方状态库与并发渲染的数据同步
  • useInsertionEffect:用于 CSS-in-JS 库优化样式注入性能

并发渲染实现原理

问题背景

React 15 中,状态更新触发的渲染无法中断。如果组件树很大,遍历和 diff 计算会长时间占用主线程,导致页面卡顿。

解决思路

  • 将渲染任务拆分成可中断的小任务
  • 每个任务执行时间控制在 5ms 左右
  • 超时后暂停,将主线程交给浏览器处理 UI 渲染和用户交互
  • 在后续帧中继续执行未完成的任务
  • 为任务分配优先级,优先执行高优任务

Fiber 架构

Fiber 是一种链表数据结构,每个 Fiber 节点包含:

{
  stateNode,  // 组件实例或 DOM 元素
  child,      // 子节点
  sibling,    // 兄弟节点
  return,     // 父节点
  alternate,  // 连接 current 树和 workInProgress 树
  // ...
}

React 维护两棵 Fiber 树:

  • Current Fiber 树:当前屏幕显示的内容
  • workInProgress Fiber 树:正在内存中构建的新树

两棵树通过 alternate 属性连接,构建完成后切换指针即可完成更新。

渲染流程

渲染分为两个阶段:

  1. Render 阶段(可中断):遍历 Fiber 树,计算变化,更新 workInProgress 树
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  1. Commit 阶段(不可中断):将 workInProgress 树作为 current 树,更新 DOM

时间切片

shouldYield 函数判断是否需要中断:

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  
  if (timeElapsed < 5) {
    // 执行时间小于 5ms,继续执行
    return false;
  }
  
  // 执行时间过长,检查是否有高优任务
  // 如用户输入、绘制等
  return true;
}

中断后,React 使用 MessageChannel 创建宏任务,在后续帧中继续执行:

const channel = new MessageChannel();
const port = channel.port2;

channel.port1.onmessage = () => {
  // 继续执行未完成的任务
  workLoop();
};

// 中断时发送消息
port.postMessage(null);

优先级控制

React 18 使用 Lanes 模型管理优先级,不同更新对应不同的 Lane:

  • SyncLane:同步更新,最高优先级(如用户输入)
  • DefaultLane:默认优先级
  • TransitionLane:过渡更新,低优先级
  • RetryLane:重试更新,最低优先级

当高优先级更新到来时,会中断低优先级更新,优先处理高优任务。

升级指南

1. 使用新的根节点 API

// 旧版本仍然兼容,但只有使用 createRoot 才能启用新特性
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

2. TypeScript 类型更新

需要显式声明 children 类型:

interface MyButtonProps {
  color: string;
  children?: React.ReactNode;
}

3. 不再支持 IE

React 18 放弃了对 Internet Explorer 的支持。

关键点

  • 自动批处理扩展到 Promise、setTimeout 等异步操作中,减少不必要的渲染
  • Transitions 通过 startTransitionuseTransition 区分紧急和非紧急更新,提升交互响应速度
  • Suspense 在 React 18 中基于并发渲染重新实现,支持流式 SSR 和逐步 hydration
  • Fiber 架构通过双缓存机制和链表结构实现可中断的渲染,每个任务执行时间控制在 5ms 左右
  • Lanes 优先级模型允许高优任务中断低优任务,确保用户交互的及时响应