#Next.js#React

React 并发特性深入:Suspense、Transitions 与 useDeferredValue

React 18+ 的并发渲染不是"多线程",是"可中断渲染"。本文讲清三个核心并发特性:Suspense(声明式加载状态)、useTransition(把重更新标为低优先级不阻塞输入)、useDeferredValue(延迟昂贵计算),以及它们怎么让 React 应用在不牺牲响应性的前提下做更多事。

本文要点

  • 主题聚焦:Next.js、React
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月23日·更新于 2026年7月23日

写过几年 React 的人,大概都经历过这种场景:一个搜索框,用户输入的时候页面卡半秒;一个表格切了几万条数据,点击按钮后整个界面"冻住";一个路由切过去,白屏转圈,连返回按钮都点不动。

这些问题的根因是同一个——React 的渲染曾经是同步的、不可中断的。一旦开始 reconcile 一棵大树,主线程就被霸占到结束,用户的输入、点击、动画全部排队等候。

React 18 把"并发特性(Concurrent Features)"扶正,就是要解决这个问题。但社区里对它的误解一直不少:有人以为 React 变成多线程了,有人把 useTransition 当万金油到处套,有人把 <Suspense> 包到根节点上然后奇怪为什么瀑布流跑不出来。

这篇文章把这些一次讲透:并发渲染到底改了什么、三个核心 API(Suspense / useTransition / useDeferredValue)各自的边界、它们怎么配合、什么时候不该用。


一、并发渲染的本质:不是多线程,是"可中断的渲染"

先把最根上的误解澄清掉。

React 并发渲染 ≠ 多线程渲染。 React 的渲染依然跑在 JS 主线程里,依然单线程。它没有派 worker 去并行画组件,也没有把 reconcile 过程丢到别的线程。

那"并发"到底并发在哪?并发在调度。具体来说是三件事:

  1. 可中断(Interruptible):渲染一棵树的过程中,如果有更高优先级的任务进来(比如用户输入),React 可以暂停当前渲染。
  2. 可恢复(Resumable):暂停之后,处理完高优先级任务,React 可以从断点继续,而不是从头再来。
  3. 优先级调度(Priority Scheduling):不同的更新有不同的优先级。用户输入是高优先级(ContinuousEvent / UserBlocking),数据获取、setState 的批量结果是低优先级(Default / Idle)。

这一切的底层是 Fiber 架构在 16 就埋下的种子。Fiber 把组件树拆成可中断的工作单元,每个 fiber 节点都可以 pause / resume。但 16/17 时代,这套机制只在 shouldComponentUpdate / unstable_batchedValues 这种局部露出水面,普通开发者摸不到。

到了 18,并发模式从 opt-in 的 unstable_ConcurrentMode 变成默认开启,配套的 API(createRoot、Suspense 数据获取、useTransition、useDeferredValue、useSyncExternalStore)才真正可用。

一个直观的对比

同步渲染时代,下面这段代码会卡:

tsx
function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setQuery(e.target.value);
    // 假设 filterResults 是一个 200ms 的昂贵计算
    setResults(filterResults(e.target.value));
  }

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

每次按键,React 都要同步完成"更新 input → 计算 results → 渲染 ResultList"这一整套。200ms 期间用户什么都做不了。打字快一点就会感到明显的"粘滞感"。

并发渲染时代,React 可以说:"输入框的更新(受控组件)是高优先级,先做;结果列表的更新是低优先级,可以被打断、可以推迟。"于是输入框永远跟手,结果列表在后台慢慢追。

但这个能力不会自动生效——你得用下面这三个 API 把意图告诉 React。


二、Suspense:声明式的加载状态

<Suspense> 是并发特性里最早出现、也最好理解的一个。它解决的问题是:"这块子树还没准备好,先显示个占位"。

核心机制:throw promise

Suspense 不是魔法。它的本质是一个约定:子组件在数据没就绪时,抛出一个 promise(技术上叫"throw a promise")。最近的 <Suspense> 边界接住这个 promise,渲染 fallback,等 promise resolve 后再重新渲染子树。

tsx
import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Profile userId={1} />
    </Suspense>
  );
}

这种"声明式加载状态"的好处是:组件本身只关心"数据有就有、没有就没有",加载状态从组件内部抽离到边界上,逻辑更干净。

2.1 配合 React.lazy 做代码分割

这是最早、最稳的用法。

tsx
import { lazy, Suspense } from 'react';

const SettingsPage = lazy(() => import('./SettingsPage'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <SettingsPage />
    </Suspense>
  );
}

React.lazy 加载 chunk 时 throw 一个 promise,Suspense 显示 fallback,chunk 下载完后重新渲染。从代码角度看,组件依然写成一个同步函数,没有 if (loading) return ... 这种条件分支。

2.2 配合数据获取

这才是 Suspense 真正的设计意图。React 团队一直想让"数据获取"也能 throw promise,从而把加载状态彻底从业务组件里挤出去。

社区里 React Query、SWR、Relay 都支持这个模式。以 React Query 为例(启用 suspense: true):

tsx
function Profile({ userId }: { userId: number }) {
  // 这个 hook 内部在数据未就绪时 throw promise
  const { data } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  return <div>{data.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Profile userId={1} />
    </Suspense>
  );
}

组件代码里没有任何 loading 分支,业务逻辑非常干净。但这有代价:你必须把加载状态交给外层的 Suspense,如果边界放得不对,会出现"整个页面白屏转圈"的体验。下面会专门讲边界放哪。

2.3 嵌套 Suspense:避免瀑布,允许并行

这是实战里最容易踩坑的地方。看下面两段代码的差别:

瀑布式(差):

tsx
<Suspense fallback={<PageSpinner />}>
  <Header />
  <Suspense fallback={<BodySpinner />}>
    <Body />
    <Suspense fallback={<SidebarSpinner />}>
      <Sidebar />
    </Suspense>
  </Suspense>
</Suspense>

如果你把所有 Suspense 都写在一起,但数据请求是在子组件 useEffect 里发的——典型的"组件挂载才发请求"——那就会形成瀑布:父组件数据回来 → 挂载子组件 → 子组件发请求 → 等待 → 孙组件挂载……每一层都串行等待。

并行式(好):

tsx
function App() {
  return (
    <Suspense fallback={<PageSpinner />}>
      <Header />
      <Suspense fallback={<BodySpinner />}>
        <Body />
      </Suspense>
      <Suspense fallback={<SidebarSpinner />}>
        <Sidebar />
      </Suspense>
    </Suspense>
  );
}

如果 Body 和 Sidebar 的请求是同时触发的(比如用了 React Query 的 prefetch、或者数据获取框架的并行声明),那这两个 Suspense 边界会并行 resolve,谁先回来谁先显示,互不阻塞。

这是 Suspense 最大的价值:它让"并行加载"成为一种默认的、声明式的结构,而你不需要手写 Promise.all。

2.4 Streaming SSR 中的 Suspense

如果你用 Next.js(App Router)或者 Remix,Suspense 还承担了 SSR 流式渲染的职责。服务端不需要等所有数据都拿到才返回 HTML——遇到 <Suspense> 就先吐出 fallback,等数据 ready 再通过同一个 HTTP 流把真实内容以 <script> 形式注入。

效果:首屏 TTFB 极快,慢组件后续逐步"补全",用户不需要盯着白屏。

tsx
// Next.js App Router 里,每个 page.tsx 本身就在一个 Suspense 边界里
export default function Page() {
  return (
    <>
      <Hero /> {/* 立即可用 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments /> {/* 数据慢,流式补全 */}
      </Suspense>
    </>
  );
}

Suspense 小结

  • 价值:把加载状态从组件内部抽到边界,让业务组件回归"纯渲染"。
  • 用法:React.lazy(最稳)、数据获取框架(React Query/SWR/Relay)、SSR 流式渲染。
  • 关键决策:边界放哪决定体验。放得太外,整个页面白屏;放得太内,瀑布流。
  • 别忘了:Suspense 不是性能优化,它是加载状态的语义化抽象。

三、useTransition:把"重的更新"标为低优先级

useTransition 是并发特性里最实用的一个。它的定位是:让某个状态更新可以被打断,不阻塞高优先级交互。

API 长这样

tsx
const [isPending, startTransition] = useTransition();

function onChange(e) {
  setQuery(e.target.value); // 高优先级:受控输入必须立即生效
  startTransition(() => {
    setResults(filterEverything(e.target.value)); // 低优先级:结果可以延迟
  });
}

startTransition 里包的 setState 会被标记成 transition,优先级降到 Default 以下。在渲染过程中,如果有更高优先级的更新进来(用户继续打字、点击按钮),React 会扔掉这次 transition 渲染,等高优先级处理完再重新开始。

注意"扔掉"——这是关键。同步渲染时代,一旦开始渲染就要渲染完,即使中间用户已经输入了 5 个字符,React 也得把第 1 个字符的结果算完。transition 模式下,那个被作废的中间结果根本不会被提交。

完整的搜索框实战

这是 useTransition 最经典的场景:

tsx
import { useState, useTransition, useMemo } from 'react';

interface Item {
  id: string;
  name: string;
}

export function SearchBox({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Item[]>(items);
  const [isPending, startTransition] = useTransition();

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setQuery(value); // 高优先级:输入框立即更新
    startTransition(() => {
      // 低优先级:把昂贵的过滤标记为 transition
      setResults(items.filter((i) => i.name.includes(value)));
    });
  }

  return (
    <div>
      <input value={query} onChange={onChange} />
      {isPending && <span className="text-gray-400">过滤中…</span>}
      <ul>
        {results.map((r) => (
          <li key={r.id}>{r.name}</li>
        ))}
      </ul>
    </div>
  );
}

对比最开头那个卡顿的版本,输入框永远跟手,结果列表在后台慢慢追。isPending 还能用来显示一个微弱的 loading 提示,告诉用户"结果还在算"。

3.1 它和 debounce 的本质区别

这是面试和实战都常被问的问题。

debounce:用定时器延迟触发。不管主线程忙不忙,它都等到 N ms 后才执行。期间用户停手了才更新。

useTransition:不延迟,立即开始渲染,但允许被打断。如果主线程空闲,它会立刻执行;如果用户在持续输入,它会"追赶"——每个按键都触发一次新的 transition,旧的被丢弃。

实战上的取舍:

  • 数据获取这种网络请求,用 debounce——你不想每个字符都打一次 API。
  • 本地昂贵计算这种纯 CPU 工作,用 useTransition——它能在主线程空闲时立即完成,体验比 debounce 的固定延迟更跟手。

两者也能组合:debounce API 请求,useTransition 处理本地过滤。这才是成熟方案。

3.2 useTransition 的边界

startTransition 里只能放状态更新(setState)。直接在里面做昂贵的同步计算是没用的——因为计算本身是同步阻塞的,React 管不到。正确的做法是把计算结果用 setState 提交,让 React 调度这次提交。

tsx
// 错:昂贵计算直接放在 transition 里,仍然阻塞主线程
startTransition(() => {
  const r = heavyCompute(value); // 这一步同步执行,不受 transition 影响
  setResults(r);
});

// 对:把计算挪到 setState 里,或者用 useMemo + useDeferredValue 配合

这是社区里最常见的误用。startTransition 不是"让这段代码异步化",它只是"让这次 setState 低优先级"。代码本身的执行依然是同步的。

useTransition 小结

  • 价值:让重的状态更新可被打断,保护高优先级交互(输入、点击、动画)。
  • 适用:本地昂贵计算驱动的更新(搜索过滤、列表排序、tab 切换大数据视图)。
  • 不适用:网络请求(用 debounce/缓存框架)、纯同步计算(先把结果存 state)。
  • 配合 isPending 给用户反馈。

四、useDeferredValue:延迟一个值的传播

useDeferredValue 是 useTransition 的"镜像"。它从值的视角解决同一个问题。

API

tsx
const deferredQuery = useDeferredValue(query);

query 变化时,deferredQuery 不一定立即跟着变。React 会判断:当前渲染是不是太忙?如果是,就先保留旧值,把新值的传播推迟到下次空闲渲染。

听起来玄,看例子就懂。

大数据表格实战

tsx
import { useState, useDeferredValue, useMemo } from 'react';

interface Row {
  id: string;
  /* ...很多字段 */
}

export function BigTable({ rows }: { rows: Row[] }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);

  // 昂贵的计算依赖的是 deferredFilter,而不是 filter
  const filtered = useMemo(
    () => rows.filter((r) => matches(r, deferredFilter)),
    [rows, deferredFilter]
  );

  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
      />
      <table className={isStale ? 'opacity-60' : ''}>
        <tbody>
          {filtered.map((r) => (
            <Row key={r.id} data={r} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

这里的精髓是:input 用的 filter 是"新"的,表格用的 deferredFilter 是"旧"的。输入框永远立即响应,表格的过滤计算可以延迟、可以被打断。isStale 标记让 UI 表达"表格正在追"。

4.1 useTransition vs useDeferredValue:什么时候选哪个

它们解决的是同一类问题,但视角不同:

维度useTransitionuseDeferredValue
视角"这次 setState 是低优先级""这个值的传播可以延迟"
触发点setState 的生产端value 的消费端
何时使用你能控制 setState 的调用位置你只能拿到一个值(比如来自 props、来自第三方 store)
配套自带 isPending需要自己对比 value !== deferredValue 得到 stale 状态
适用组件当前组件 setState子组件 / 接收 prop 的组件

实战经验法则:

  • 能改 setState 那行代码 → 用 useTransition。
  • 值是从 props 或外部传进来的,你改不了源头 → 用 useDeferredValue。

举个例子:你封装了一个 DataTable,外部传 query 进来。你没法去外层包 startTransition,这时候在 DataTable 内部 useDeferredValue(query) 是最干净的解法。

4.2 和 debounce/throttle 的区别

跟 useTransition 类似:

  • debounce/throttle 基于时间,固定延迟,不管主线程状态。
  • useDeferredValue 基于调度,主线程闲就立即更新,忙就推迟。

对本地计算来说,useDeferredValue 通常体验更好——不需要人为引入延迟,用户在停下来时立刻看到结果。

useDeferredValue 小结

  • 价值:从"值消费端"延迟昂贵计算的触发,保护输入响应。
  • 适用:表格过滤、图表重绘、依赖 prop 的复杂派生计算。
  • 与 useTransition 互补:生产端可控用 transition,消费端可控用 deferred。
  • 别忘了配合 useMemo,否则延迟了值的传播但没缓存计算,等于白干。

五、三者怎么配合,以及什么时候不要用

讲到这里你应该能感觉到:这三个 API 不是孤立的,它们都在围绕"让昂贵的渲染可被打断"这一件事做不同的切面。

5.1 典型组合

Suspense + useTransition 是黄金搭档。看这个路由切换的例子(Next.js App Router 或者自研路由都适用):

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

  function switchTab(next: string) {
    startTransition(() => {
      setTab(next);
    });
  }

  return (
    <div>
      <Nav active={tab} onSwitch={switchTab} isPending={isPending} />
      <Suspense fallback={<RouteSpinner />}>
        {isPending ? <RouteSpinner /> : renderTab(tab)}
      </Suspense>
    </div>
  );
}

切换 tab 时,新的 tab 组件可能在请求数据(throw promise)。如果只用了 Suspense,切 tab 的瞬间整个区域会立刻 fallback 成 spinner——视觉上很跳。把 setTab 包在 startTransition 里之后,旧 tab 会保持显示直到新 tab 数据就绪,过渡平滑得多。isPending 还能让 Nav 显示"加载中"的微弱状态。

这是 React 官方推荐的路由切换姿势,也是 Next.js App Router router.push 默认就是 transition 的原因。

useDeferredValue + useMemo 是大数据组件的标准结构,上面已经展示过。

5.2 一个完整的实战:实时搜索 + 路由切换

把三者合到一个真实组件里:

tsx
function ProductSearch({ query }: { query: string }) {
  // query 来自 props(比如 URL search param),源头不可控 → deferred
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  // useMemo 锁住昂贵计算
  const results = useMemo(
    () => expensiveFilter(deferredQuery),
    [deferredQuery]
  );

  return (
    <div className={isStale ? 'opacity-70' : ''}>
      <Suspense fallback={<GridSkeleton />}>
        <ProductGrid products={results} />
      </Suspense>
    </div>
  );
}

这里 useDeferredValue 保护输入响应,useMemo 缓存计算,Suspense 兜住可能 throw promise 的子组件(比如 ProductGrid 里某些图片懒加载)。各司其职。

5.3 取舍:不要到处用

这三个 API 都有成本——不是性能成本,是认知成本。每多一个 startTransition / useDeferredValue,读者就得多想一层"这个值是新的还是旧的、这次更新是不是被延迟了"。

判断要不要用的标准:

  1. 真的卡了吗? 用 React DevTools 的 Profiler 录一下,看看渲染时长。100ms 以内的更新,绝大多数时候用不上这些 API。React 本身的 batch 已经够用。
  2. 卡的根因是渲染吗? 如果卡的是网络请求,请用 debounce + 缓存;如果卡的是巨大的同步计算(比如 100 万次的纯 CPU),React 的并发调度救不了你,得用 Web Worker 或 OffscreenCanvas。
  3. 加完之后更难读了吗? 如果团队里 80% 的人看不懂这段 transition 的意图,那它的代价就超过了收益。

我的经验:这两个 hook 在 5% 的组件里用得到,剩下 95% 该用 debounce 用 debounce、该用缓存用缓存、该拆组件拆组件。把并发 API 当银弹是新人最常见的过度工程。


六、三个典型实战场景拆解

场景 1:实时搜索(useTransition)

需求:用户在输入框打字,下方结果列表实时过滤。数据 1 万条,本地过滤。

关键点:

  • 输入框的 value 用 query(高优先级),保证跟手。
  • 过滤结果用 startTransition 包裹,标记为低优先级。
  • isPending 给一个微弱的 loading 提示,不要让用户以为界面卡了。

代码见第三章。如果数据再大一点(10 万条),建议同时上 useMemo + Web Worker,把过滤挪出主线程,transition 只负责让"提交结果"这一步可被打断。

场景 2:大数据表格(useDeferredValue + useMemo)

需求:一个表格展示几千行数据,用户输入过滤条件,表格重排重渲染很慢。

关键点:

  • filter 是受控 input 的 state。
  • deferredFilter = useDeferredValue(filter),表格内部用的是 deferred 版本。
  • useMemo 依赖 deferredFilter,否则延迟没意义。
  • isStale 判断做视觉降级(透明度、骨架纹),告诉用户"表格在追"。

代码见第四章。如果行渲染本身也很重(每行有复杂组件),考虑虚拟列表(react-virtual / TanStack Virtual)一起上——并发特性优化的是"渲染可中断",虚拟列表优化的是"渲染总量",两者不冲突,叠加用。

场景 3:路由级 Suspense(Next.js App Router)

需求:App Router 切到数据密集的页面,希望首屏快、过渡顺。

关键点:

  • Next.js App Router 默认把每个 page.tsx 包在 Suspense 边界里,loading.tsx 就是 fallback。这个不用你写。
  • 页面内部把"慢的子组件"再单独包 <Suspense>,让快的内容先出来。
  • 主动路由跳转用 useRouter().push 包在 startTransition 里(或者直接用 useTransition 配合 Link 的 prefetch),让旧页面保持显示直到新页面就绪。
tsx
// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <>
      <Header /> {/* 立即可用 */}
      <Suspense fallback={<ChartSkeleton />}>
        <SlowChart /> {/* 数据获取慢,流式补全 */}
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <DataTable /> {/* 与图表并行加载 */}
      </Suspense>
    </>
  );
}

App Router + Streaming SSR 的组合下,这个页面的 HTML 会分块返回:Header 先到,Chart 和 Table 各自的数据好了之后再补。用户首屏看到内容的速度远快于"等所有数据"的传统 SSR。


七、常见误用和陷阱

最后总结几个我反复在 code review 里挑出来的问题。

7.1 把 startTransition 当成"异步化"

tsx
// 错
startTransition(async () => {
  const data = await fetch('/api/x');
  setData(data);
});

startTransition 的回调里不应该有 await。它只关心同步执行的 setState 调用。异步代码里再 setState,那已经是微任务之后的事,transition 标记丢了。正确做法:

tsx
// 对
fetch('/api/x').then((data) => {
  startTransition(() => {
    setData(data);
  });
});

把 transition 包在 setState 外面,而不是包在整个异步操作外面。

7.2 把 Suspense 边界放在根节点

tsx
// 错
<Root>
  <Suspense fallback={<FullPageSpinner />}>
    <Header />
    <Nav />
    <Main />
    <Footer />
  </Suspense>
</Root>

任何一个子组件 throw promise,整个页面变成 spinner。哪怕 Header / Nav 这些本来就立即可用的部分也被拖下水。

Suspense 边界应该放在**"加载状态有独立语义"的层级**——一个 widget、一个 panel、一个 route section。粒度越细,并行度越高,体验越好。

7.3 在 transition 里又混入同步 setState

tsx
startTransition(() => {
  setQuery(value);  // 这个也会变成低优先级!
  setResults(filter(value));
});

如果你把受控 input 的 state 也包进了 transition,输入框就会变得"粘滞"——React 会认为它也是低优先级,可以延迟提交。这正好抵消了你想优化的东西。

记住:受控组件的 state 永远不要进 transition。

7.4 useDeferredValue 不配 useMemo

tsx
const deferred = useDeferredValue(query);
// 每次都重算,deferred 没有任何意义
const results = heavyFilter(deferred);

useDeferredValue 推迟的是"值的传播",但如果你不缓存派生计算,React 还是会算——只是晚一点点算而已。必须配合 useMemo(或者把计算挪进 setState),延迟才有实际收益。

7.5 过度使用

最隐蔽的问题。有些团队学了并发特性之后,所有 useState 都包 startTransition,所有 prop 都过一遍 useDeferredValue。结果代码到处是"这个值可能是旧的"的隐式假设,调试的时候自己都搞不清哪次渲染用的是新值。

并发 API 是手术刀,不是撒盐。只在 Profiler 真的显示了渲染瓶颈的地方用。其它地方老老实实写同步代码,可读性远比"理论上更优"重要。

7.6 忘了 useSyncExternalStore

如果你用了非 React 的状态管理(zustand、Redux、Jotai 等),在并发模式下可能会出现"tearing"——同一次渲染中,不同组件读到了不同时刻的 store 值。这是并发渲染可中断带来的副作用。

解法不是不用这些库,而是确认它们用了 useSyncExternalStore 来订阅外部 store(主流库从 2022 年起都已迁移)。自己在写外部 store 集成时也要用 useSyncExternalStore,不要手撸 useEffect + setState 的订阅模式。


小结

把全文压缩成几条可以直接带走的:

  1. 并发渲染不是多线程,是"可中断 + 可恢复 + 优先级调度"的渲染调度模型,跑在单线程 JS 上,背后是 Fiber 架构。
  2. Suspense 是声明式加载状态的抽象。它解决的不是"加载快不快",而是"加载状态写得干不干净"。边界位置决定并行度。
  3. useTransition 标记低优先级更新,用于保护受控输入这类高优先级交互。只对 setState 起作用,不是异步化魔法。
  4. useDeferredValue 从消费端延迟值的传播,用于你改不了源头 setState 的场景(比如 prop 进来的值)。一定要配 useMemo。
  5. 三者配合 > 单打独斗。Suspense + transition 做路由切换、deferred + useMemo 做大表格,都是经过验证的组合拳。
  6. 不要到处用。Profiler 测出来真的卡的才上;网络瓶颈用缓存、CPU 瓶颈用 Worker,并发 API 只解决"渲染可中断"这一件事。

金句:并发特性的本质,是把"什么时候渲染、渲染到哪"的决定权从代码里搬到 React 调度器里——你告诉它意图(这个更新可以慢、这个值可以旧),它来安排时机。它不会让代码变快,它让代码在不快的时候不被卡住。这才是 React 18 给你的真正礼物。


写完这篇文章,再去看任何一份 React 项目的并发相关代码,你应该能一眼分辨出:哪些是真优化,哪些是"为了显得高级而包了一层 transition"。后者比前者多得多。能克制地用工具,比能用工具更接近资深。

Related

相关推荐