FumadocsZDecode
React

React 18 并发特性

React 18 的核心升级不是新增了几个 API,而是把渲染从「同步阻塞」变成「可调度」——render 可以暂停、恢复、丢弃、重试、插队。所有并发特性都为同一个目标服务:用户交互优先,页面更流畅,避免主线程长时间阻塞。


一、Concurrent:底层调度能力

并发不是「多线程」,而是一套底层渲染机制:渲染过程可中断、可恢复、有优先级

React 18 之前渲染是同步的,一旦开始必须执行完。组件树很大时(如 render 耗时 50ms),浏览器就会卡顿。并发模式下,React 借助 Fiber + 时间切片把渲染拆成多个小任务,浏览器有空再继续,做到「边干活边响应用户」。

能力说明
可中断render 可以暂停
可恢复下次继续 render
优先级用户输入优先
可丢弃旧 render 不一定提交

启用方式是用新的 createRoot

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

function App() {
  return <h1>Hello React 18</h1>
}

const root = createRoot(document.getElementById('root')!)
root.render(<App />)

二、Automatic Batching:自动批处理

批处理就是把多个 state 更新合并成一次重新渲染。

React 18 之前,只有 React 事件处理函数内的更新会被批处理;PromisesetTimeout、原生事件回调里的更新不会,会触发多次渲染。React 18 把批处理扩大到所有场景:

function Counter() {
  const [count, setCount] = React.useState(0)
  const [flag, setFlag] = React.useState(false)

  const handleClick = () => {
    setTimeout(() => {
      // React 18 之前:渲染 2 次;React 18:合并成 1 次
      setCount((c) => c + 1)
      setFlag((f) => !f)
    }, 0)
  }

  return <button onClick={handleClick}>{count}</button>
}

需要强制同步更新(如更新后立刻读取 DOM 布局)时,用 flushSync 退出批处理:

import { flushSync } from 'react-dom'

function Demo() {
  const [count, setCount] = React.useState(0)

  const handleClick = () => {
    // 包在 flushSync 里的更新会立即同步刷新 DOM
    flushSync(() => setCount((c) => c + 1))
    // 此处可以读到更新后的 DOM
  }

  return <button onClick={handleClick}>{count}</button>
}

三、Transition:过渡更新

useTransition 用来标记非紧急更新,告诉 React 这个更新可以被打断,让紧急更新(如输入框输入)优先。

典型场景是输入框搜索:输入要立即响应,但结果列表渲染可以延迟。

import { useState, useTransition } from 'react'

function Search() {
  const [input, setInput] = useState('')
  const [list, setList] = useState<string[]>([])
  const [isPending, startTransition] = useTransition()

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value) // 紧急:输入框立即更新
    startTransition(() => {
      // 非紧急:可被打断,列表慢一点无所谓
      setList(filterHugeList(e.target.value))
    })
  }

  return (
    <div>
      <input value={input} onChange={onChange} />
      {isPending && <span>加载中...</span>}
      <ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
    </div>
  )
}

useTransition 返回 [isPending, startTransition]isPending 表示是否过渡中(可显示加载态),startTransition 开启低优先级更新。


四、useTransition vs useDeferredValue

两者都把更新降级为非紧急,核心区别在控制点不同

对比useTransitionuseDeferredValue
控制对象更新函数(setState)某个值
用法startTransition(() => setX(v))useDeferredValue(value)
适用改得了更新源头值来自 props/父组件/第三方,改不了源头
import { useDeferredValue, useMemo } from 'react'

// 值是别人传进来的,控制不了源头 → 用 useDeferredValue
function List({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query)

  // query 立即更新,deferredQuery 延迟更新,所以输入不卡
  const items = useMemo(() => filterHugeList(deferredQuery), [deferredQuery])

  return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>
}

一句话:改得了更新逻辑用 useTransition,只拿得到值用 useDeferredValue


五、Suspense 改进

Suspense 不是 18 才有(16.6 就能用于 React.lazy),但 18 增强了它,支持SSR 流式渲染 + 选择性 hydration

import { Suspense, lazy } from 'react'

const SlowComponent = lazy(() => import('./SlowComponent'))

function Page() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <SlowComponent />
    </Suspense>
  )
}

关键改进:服务端可以先发送已就绪的部分 HTML,慢的部分用 Suspense 占位,后续再流式补上,不必等整个页面就绪。


六、Tearing(撕裂)与 useSyncExternalStore

撕裂指同一次渲染中,不同组件读到了同一个外部数据源的不同值,导致 UI 不一致。

为什么并发模式才有这问题:同步渲染一气呵成,渲染期间外部数据不会变。但并发渲染可以中断

React 自己的 state(useState)不会撕裂——React 保证一致性。问题只出在外部可变数据源(Redux 早期实现、Zustand、自写全局 store)。

useSyncExternalStore 就是为解决撕裂而生,专门订阅外部 store:

import { useSyncExternalStore } from 'react'

function useStore(store: any) {
  return useSyncExternalStore(
    store.subscribe,        // 订阅函数,store 变化时触发回调
    store.getSnapshot,      // 读取当前值
    store.getServerSnapshot // SSR 时的值(可选)
  )
}

它的保证:并发渲染中若检测到外部 store 中途变了,React 会放弃这次渲染、同步重渲染,强制所有组件读到同一个值。Redux、Zustand 在 React 18 都改用了这个 API,把一致性交给 React。


七、四个特性的关系

它们是一套体系,Concurrent 是底层能力,其余建立在它之上:

问题对应方案
render 中断Concurrent
更新优先级Transition
异步协调Suspense
更新合并Automatic Batching
外部状态一致性useSyncExternalStore

八、常见追问

为什么 startTransition 里不能放异步逻辑? 回调必须是同步的 setState。如果里面 await,await 之后的更新就脱离了 transition 上下文,不再被标记为非紧急(React 19 用 Actions 改善了这点)。

Transition 会丢更新吗? 不会。被打断只是延后,最终一定执行完。

自动批处理如何退出?flushSync(() => setState(...)),包在里面的更新立即同步刷新 DOM。


核心一句话:React 18 让 render 从「一旦开始不可中断」升级为「可暂停、可恢复、可丢弃、可插队」。Concurrent 是底层调度能力,Transition 控制优先级、Suspense 协调异步、Automatic Batching 合并更新、useSyncExternalStore 保证外部状态一致——整套设计都是为了「用户交互优先、页面更流畅」。

On this page