React useTransition 和 useDeferredValue 面试题整理

React useTransitionuseDeferredValue 面试题整理

一、先建立整套知识框架

1. 这两个 Hook 到底解决什么问题?

核心思路(一句话)

核心不是"让 React 变快",而是把"必须立刻响应的更新"和"可以稍后完成的昂贵更新"区分开,让昂贵更新不要阻塞用户交互。


2. 这道题的主要矛盾是什么?

假设页面有两个操作:

text 复制代码
用户输入搜索关键词
        ↓
输入框立即显示字符
        +
根据关键词筛选 10 万条数据
        ↓
大量组件重新渲染
        ↓
CPU 消耗很高
        ↓
输入框也跟着卡顿

这里真正的矛盾不是"数据太多"本身,而是:

text 复制代码
高优先级任务:用户交互反馈
        VS
低优先级任务:昂贵 UI 更新

主要矛盾

用户希望立即得到反馈,但 React 同时需要处理一项昂贵的渲染工作。

次要矛盾

如果昂贵组件本身还存在不必要的重复渲染,那么单纯调度优先级仍然不够。

所以性能优化通常应该分成两层:

text 复制代码
第一层:减少"不必要"的工作
        ↓
React.memo / useMemo / useCallback / 状态下沉 / 虚拟列表

第二层:安排"必须做"的工作
        ↓
useTransition / useDeferredValue

这是整个知识点最重要的框架。


二、面试题 1:什么是 useTransition?

核心思路(一句话)

useTransition 用来把某些状态更新标记成非紧急的 Transition,让 React 可以在后台处理这些更新,并在更紧急的交互发生时优先处理交互。

React 官方定义中:

javascript 复制代码
const [isPending, startTransition] = useTransition();

它返回两个东西:

text 复制代码
isPending
    ↓
当前是否存在正在进行的 Transition

startTransition
    ↓
把状态更新标记为 Transition

官方明确说明,Transition 更新是 non-blocking 的,并且可以被其他更紧急的状态更新中断。(React)


解决方案流程图

text 复制代码
用户操作
   ↓
事件处理函数
   ↓
┌───────────────────────────────┐
│ 是否属于必须立即反馈的状态?   │
└───────────────────────────────┘
        ↓是                    ↓否
        ↓                     ↓
普通 setState          startTransition(...)
        ↓                     ↓
高优先级更新             Transition 更新
        ↓                     ↓
立即响应                 后台渲染
                              ↓
                    如果发生新的紧急更新
                              ↓
                    暂停 / 丢弃当前渲染
                              ↓
                    优先处理新的交互
                              ↓
                    再重新处理最新 Transition

三、面试题 2:useTransition 的底层实现原理是什么?

核心思路(一句话)

本质是"改变更新的调度优先级",而不是"开启一个新线程"或者"设置一个延时器"。

这是非常容易被面试官追问的一题。


1. 传统同步渲染有什么问题?

假设:

text 复制代码
用户输入 A
   ↓
setState
   ↓
React 开始计算整个大列表
   ↓
几十万次组件计算
   ↓
DOM 提交

如果这段渲染工作一直占着主线程,那么用户后续输入:

text 复制代码
B
C
D

都可能无法及时响应。


2. React 18 引入并发渲染之后发生了什么?

React 18 的并发渲染允许 React:

text 复制代码
开始渲染
   ↓
执行一部分 Fiber 工作
   ↓
发现更高优先级更新
   ↓
暂停 / 中断当前工作
   ↓
优先处理更紧急的更新
   ↓
再恢复或者重新开始之前的渲染

React 官方明确描述了这种能力:并发渲染下,React 可以开始渲染、暂停、稍后继续,甚至直接放弃一个尚未完成的渲染,并最终只在完成后提交一致的 DOM。(React)


3. startTransition 做了什么?

例如:

javascript 复制代码
startTransition(() => {
    setSearchQuery(value);
});

React 不会简单理解成:

text 复制代码
"500 毫秒后执行"

也不是:

text 复制代码
"启动 Web Worker"

而是:

text 复制代码
这个更新属于 Transition
        ↓
把它放入低优先级的并发更新体系
        ↓
允许它被更紧急的更新打断

从 React 内部实现角度,可以把它理解为:

text 复制代码
setState
  ↓
创建 Update
  ↓
分配对应的 Lane / 优先级
  ↓
Root 记录有哪些 Lane 正在等待处理
  ↓
Scheduler 选择当前应该处理的工作
  ↓
Concurrent Renderer 执行 Fiber 工作
  ↓
如果出现更高优先级更新
  ↓
暂停 / 丢弃当前未提交的工作
  ↓
先处理更高优先级更新

面试里说到 Lane、Scheduler、Fiber 可中断渲染 就已经进入比较深入的层次了。

但要注意:

Lane 是 React 内部实现细节,具体 Lane 类型和内部实现可能随 React 版本变化,所以面试时不要把某一个具体 Lane 名称当成永远不变的公共 API。


四、面试题 3:useTransition 返回什么?isPending 有什么作用?

核心思路(一句话)

startTransition 负责"标记",isPending 负责"感知当前 Transition 是否还没完成"。

javascript 复制代码
const [isPending, startTransition] = useTransition();

isPending

表示:

text 复制代码
是否存在正在处理中的 Transition

最常见用途:

text 复制代码
Transition 开始
   ↓
isPending = true
   ↓
显示轻量反馈
   ↓
Transition 完成
   ↓
isPending = false

例如:

javascript 复制代码
<button disabled={isPending}>
    {isPending ? '切换中...' : '切换页面'}
</button>

官方文档也明确说明,useTransition 与单独的 startTransition 的主要区别之一,就是它可以提供 isPending 状态。(React)


五、面试题 4:为什么输入框不能放进 Transition?

核心思路(一句话)

输入框本身属于用户直接操作反馈,必须保持高优先级;可以把"根据输入产生的昂贵 UI 更新"标记为 Transition,但不能把受控输入值本身降级。

这是很高频的坑。


错误写法

javascript 复制代码
startTransition(() => {
    setInputValue(value);
});

这里:

text 复制代码
用户输入字符
    ↓
input value 更新
    ↓
被标记成 Transition
    ↓
输入本身可能出现延迟

这违背了 Transition 的使用原则。

React 官方明确指出:Transition 更新不能用于控制文本输入。 (React)


正确写法

text 复制代码
用户输入
   ↓
setInputValue(value)
   ↓
立即更新输入框

同时

startTransition(() => {
    setSearchQuery(value)
})
   ↓
延迟处理昂贵列表

也就是:

text 复制代码
输入框
高优先级
   │
   ├──────────────→ 立即显示字符
   │
   ↓
搜索结果
低优先级
   │
   ↓
后台渲染

六、面试题 5:请写一个 useTransition 解决搜索大列表卡顿的完整例子

核心思路(一句话)

把输入状态和列表查询状态拆开:输入立即更新,查询结果通过 Transition 更新。

完整代码

jsx 复制代码
import { useMemo, useState, useTransition } from 'react';

/**
 * 模拟非常大的数据集。
 * 实际项目中可能来自接口、状态管理器或者后端返回的数据。
 */
const bigList = Array.from({ length: 100000 }, (_, index) => ({
  id: index,
  name: `商品-${index}`,
}));

/**
 * 大列表组件。
 *
 * 这里模拟真实项目中比较重的列表渲染。
 */
function SearchResultList({ items, query }) {
  /**
   * 根据 query 筛选数据。
   *
   * 注意:
   * 这里的计算仍然可能很耗时。
   * useTransition 并不会降低 filter 的算法复杂度,
   * 它只是让这次更新不再阻塞更紧急的交互。
   */
  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.name.includes(query)
    );
  }, [items, query]);

  return (
    <div>
      <p>匹配数量:{filteredItems.length}</p>

      {filteredItems.slice(0, 2000).map((item) => (
        <div key={item.id}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

export default function SearchPage() {
  /**
   * inputValue:
   * 用户输入框当前真正显示的值。
   *
   * 它属于高优先级更新。
   */
  const [inputValue, setInputValue] = useState('');

  /**
   * searchQuery:
   * 真正用于驱动大列表搜索的关键词。
   *
   * 这个更新可以稍后完成,因此可以放进 Transition。
   */
  const [searchQuery, setSearchQuery] = useState('');

  /**
   * isPending:
   * 当前是否存在尚未完成的 Transition。
   *
   * startTransition:
   * 用来把状态更新标记为 Transition。
   */
  const [isPending, startTransition] = useTransition();

  function handleChange(event) {
    const nextValue = event.target.value;

    /**
     * 第一件事:
     * 立即更新输入框。
     *
     * 绝对不要把受控 input 的 value 放进 Transition。
     */
    setInputValue(nextValue);

    /**
     * 第二件事:
     * 搜索大列表属于昂贵更新。
     *
     * 因此把 searchQuery 的更新标记成 Transition。
     */
    startTransition(() => {
      setSearchQuery(nextValue);
    });
  }

  return (
    <div>
      <h2>商品搜索</h2>

      <input
        value={inputValue}
        onChange={handleChange}
        placeholder="请输入商品名称"
      />

      {isPending && (
        <span style={{ marginLeft: 8 }}>
          搜索结果更新中...
        </span>
      )}

      <SearchResultList
        items={bigList}
        query={searchQuery}
      />
    </div>
  );
}

执行过程

text 复制代码
用户输入 "r"
       ↓
setInputValue("r")
       ↓
输入框立即更新

       +

startTransition()
       ↓
setSearchQuery("r")
       ↓
标记为 Transition
       ↓
React 后台准备列表更新
       ↓
用户继续输入 "re"
       ↓
更紧急输入更新优先
       ↓
旧列表渲染工作可能被中断
       ↓
最终优先处理最新 query

React 官方也明确说明:Transition 渲染如果在处理中遇到更紧急的输入更新,React 可以中断当前工作并重新开始最新的 Transition 渲染。(React)


七、面试题 6:什么是 useDeferredValue?

核心思路(一句话)

useDeferredValue 不控制"状态怎么更新",而是对"一个已经存在的值"创建一个可以滞后的版本。

API:

javascript 复制代码
const deferredValue = useDeferredValue(value);

官方定义就是:返回 value 的 deferred 版本。(React)


最关键的理解

假设:

javascript 复制代码
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

那么可以理解成:

text 复制代码
query
最新值

deferredQuery
允许滞后的值

例如:

text 复制代码
时间 →
query:
"" → "r" → "re" → "rea" → "reac" → "react"

deferredQuery:
"" → "r" → "r" → "rea" → "rea" → "react"

它们可能短时间不同步。


八、面试题 7:useDeferredValue 的底层原理是什么?

核心思路(一句话)

当前更新先用旧的 deferred value 完成紧急渲染,然后 React 再在后台尝试使用新的 value 渲染依赖它的部分。

官方对底层过程的解释非常明确。(React)


流程图

text 复制代码
value 发生变化
      ↓
useDeferredValue(value)
      ↓
当前渲染
      ↓
继续使用旧 deferredValue
      ↓
保证当前交互尽快完成
      ↓
React 后台启动一次新的渲染
      ↓
尝试使用最新 value
      ↓
┌────────────────────────┐
│ 后台渲染期间发生新更新? │
└────────────────────────┘
        ↓是                    ↓否
        ↓                     ↓
中断当前后台渲染           完成渲染
        ↓                     ↓
使用最新值重新开始           提交 UI

非常重要:它没有固定延迟

错误理解:

text 复制代码
useDeferredValue(value)
= 延迟 300 毫秒

这是错误的。

正确理解:

text 复制代码
不是时间驱动
而是 React 调度驱动

React 官方明确指出:

useDeferredValue 本身没有固定延迟时间。 React 会在当前渲染结束后尽快尝试后台渲染;如果用户再次输入,则后台工作可能再次被打断。(React)


九、面试题 8:useDeferredValue 是不是防抖或者节流?

核心思路(一句话)

不是。防抖和节流解决"减少执行次数",useDeferredValue 解决"调整渲染优先级和时机"。

这是一个特别容易被问的问题。


对比

方案 核心机制 是否固定时间 是否减少执行次数 是否改变 React 渲染优先级
防抖 一段时间没有新事件才执行
节流 一定时间窗口内最多执行一次
useDeferredValue React 调度后台渲染 不保证

一个非常重要的边界

假设:

javascript 复制代码
const deferredQuery = useDeferredValue(query);

然后:

text 复制代码
用户输入:
a
ab
abc
abcd
abcde

useDeferredValue 不能保证只计算一次

它可能产生多次后台渲染尝试,并且在高频输入过程中不断中断、重新开始。


十、面试题 9:useDeferredValue 会不会减少网络请求?

核心思路(一句话)

不会;useDeferredValue 主要影响渲染,不负责请求去重、防抖或者取消网络请求。

React 官方明确指出:

useDeferredValue 本身并不会阻止额外的网络请求。 (React)

比如:

text 复制代码
query = "a"
query = "ab"
query = "abc"

如果你的 useEffect 是:

javascript 复制代码
useEffect(() => {
    fetch(`/api/search?q=${query}`);
}, [query]);

依然可能产生:

text 复制代码
a   → 请求
ab  → 请求
abc → 请求

所以:

text 复制代码
解决输入导致请求过多
    ↓
防抖 / 请求取消 / 请求去重 / 缓存

解决输入导致 UI 渲染卡顿
    ↓
useTransition / useDeferredValue

这两件事不能混为一谈。


十一、面试题 10:为什么 useDeferredValue 经常和 React.memo 一起使用?

核心思路(一句话)

useDeferredValue 负责让值暂时不变,memo 负责在值没变的时候真正跳过子组件渲染。

这一点非常关键,也是最应该补充的一点。

假设:

javascript 复制代码
const deferredQuery = useDeferredValue(query);

return (
    <SlowList query={deferredQuery} />
);

父组件因为:

text 复制代码
query 从 "a" → "ab"

重新渲染。

此时:

text 复制代码
query = "ab"
deferredQuery = "a"

如果 SlowList 没有 memo

text 复制代码
父组件重新渲染
      ↓
SlowList 仍然有机会重新执行
      ↓
即使 query prop 还是 "a"

而使用:

javascript 复制代码
const SlowList = memo(function SlowList({ query }) {
    // ...
});

此时:

text 复制代码
query 更新
   ↓
父组件重新渲染
   ↓
deferredQuery 暂时不变
   ↓
SlowList props 没变化
   ↓
memo 跳过 SlowList
   ↓
输入框立即完成更新

之后:

text 复制代码
后台渲染完成
   ↓
deferredQuery 从 "a" → "ab"
   ↓
SlowList props 真正变化
   ↓
SlowList 渲染

所以完整链条是:

text 复制代码
useDeferredValue
    ↓
让值暂时保持旧值
    +
React.memo
    ↓
值没变时跳过组件重新渲染
    ↓
真正实现昂贵子树延后处理

React 官方明确说明,memo 的作用是当 props 没变化时跳过组件重新渲染;默认会使用 Object.is 比较 props。(React)


十二、面试题 11:请写一个 useDeferredValue 的完整例子

核心思路(一句话)

当你无法直接控制值的更新源头,只能接收一个频繁变化的值时,可以使用 useDeferredValue 给它创建一个低优先级版本。


完整代码

jsx 复制代码
import {
  memo,
  useDeferredValue,
  useMemo,
  useState,
} from 'react';

/**
 * 创建一个非常大的数据集。
 *
 * 实际项目中可能来自接口、父组件、状态管理器等。
 */
const bigList = Array.from({ length: 100000 }, (_, index) => ({
  id: index,
  name: `商品-${index}`,
}));

/**
 * 慢列表组件。
 *
 * 使用 memo 的关键原因:
 * 父组件即使因为输入框更新而重新渲染,
 * 只要 query 这个 prop 没发生变化,
 * 就可以跳过 SlowList 的重新执行。
 */
const SlowList = memo(function SlowList({ items, query }) {
  /**
   * 模拟大数据搜索。
   *
   * 这里的真正计算成本仍然存在。
   * useDeferredValue 的作用不是让 filter 更快,
   * 而是让这个计算对应的渲染不要阻塞输入框。
   */
  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.name.includes(query)
    );
  }, [items, query]);

  return (
    <div>
      <p>搜索结果:{filteredItems.length}</p>

      {filteredItems.slice(0, 2000).map((item) => (
        <div key={item.id}>
          {item.name}
        </div>
      ))}
    </div>
  );
});

export default function SearchPage() {
  /**
   * 最新输入值。
   *
   * 这个状态属于高优先级交互。
   */
  const [query, setQuery] = useState('');

  /**
   * 通过 useDeferredValue 获取 query 的延迟版本。
   *
   * 当 query 快速变化时,
   * deferredQuery 可以暂时保持旧值。
   */
  const deferredQuery = useDeferredValue(query);

  /**
   * deferredQuery !== query
   *
   * 说明:
   * 当前最新输入已经变化,
   * 但慢列表还没有跟上。
   */
  const isStale = deferredQuery !== query;

  function handleChange(event) {
    /**
     * 直接更新 query。
     *
     * 注意:
     * 这里不需要 startTransition,
     * 因为 input 自身需要立即响应。
     */
    setQuery(event.target.value);
  }

  return (
    <div>
      <h2>商品搜索</h2>

      <input
        value={query}
        onChange={handleChange}
        placeholder="请输入搜索关键词"
      />

      {isStale && (
        <span style={{ marginLeft: 8 }}>
          搜索结果更新中...
        </span>
      )}

      <SlowList
        items={bigList}
        query={deferredQuery}
      />
    </div>
  );
}

十三、面试题 12:useTransition 和 useDeferredValue 到底有什么区别?

核心思路(一句话)

useTransition 管"更新怎么发生";useDeferredValue 管"哪个值可以滞后"。

这是整套面试题最重要的结论。


第一维度:控制对象不同

useTransition

控制的是:

text 复制代码
状态更新

例如:

javascript 复制代码
startTransition(() => {
    setSearchQuery(nextQuery);
});

开发者明确告诉 React:

text 复制代码
这个状态更新可以晚一点处理

useDeferredValue

控制的是:

text 复制代码
一个值

例如:

javascript 复制代码
const deferredQuery = useDeferredValue(query);

这里的意思是:

text 复制代码
query 可以快速变化

但我允许使用 query 的这部分 UI
暂时继续使用旧 query

十四、第二维度:有没有状态更新函数的控制权

useTransition

适合:

text 复制代码
我能拿到 setState
        ↓
我自己控制更新
        ↓
我把它标记为 Transition

例如:

javascript 复制代码
const [tab, setTab] = useState('home');

startTransition(() => {
    setTab('profile');
});

useDeferredValue

适合:

text 复制代码
值来自 props / context / custom hook / 外部数据
        ↓
我不控制它怎么更新
        ↓
但这个值驱动的 UI 很慢
        ↓
我只想让"使用这个值的部分"滞后

React 官方也明确给出了这个区分:只有在你能够访问某个 state 的设置函数时,才能直接使用 startTransition 标记这个更新;如果是对 prop 或其他 Hook 返回值作出响应,则可以考虑 useDeferredValue。(React)


十五、第三维度:反馈机制不同

useTransition

直接提供:

javascript 复制代码
isPending

所以很适合:

text 复制代码
按钮加载状态
Tab 切换状态
页面导航状态
提交状态

useDeferredValue

没有:

javascript 复制代码
isPending

所以一般自己判断:

javascript 复制代码
const deferredQuery = useDeferredValue(query);

const isStale = deferredQuery !== query;

但注意:

"比较两个值判断是否过渡中"只是常见的业务侧判断方式,不应该把它理解为 useDeferredValue 官方提供的 pending 状态。


十六、第四维度:主动和被动

可以这样理解:

text 复制代码
useTransition
主动告诉 React:
"这次状态更新不紧急。"

而:

text 复制代码
useDeferredValue
声明:
"这个值我能接受暂时落后。"

十七、面试题 13:什么时候用 useTransition,什么时候用 useDeferredValue?

核心思路(一句话)

看你控制的是"更新源头"还是"消费结果"。

可以直接背下面这个判断树:

text 复制代码
性能瓶颈在哪里?
        ↓
是一个昂贵的 UI 更新?
        ↓
能不能拿到导致它变化的 setState?
        ↓
      ┌───────┴───────┐
     能                不能
     ↓                  ↓
useTransition      useDeferredValue
     ↓                  ↓
控制更新            控制值的滞后版本

十八、典型场景一:搜索和筛选大型列表

推荐思路

通常优先考虑:

text 复制代码
useTransition

因为我们一般能自己控制:

javascript 复制代码
setQuery(...)

于是:

text 复制代码
输入框状态
   ↓
立即更新

搜索结果状态
   ↓
startTransition
   ↓
后台渲染

十九、典型场景二:图表组件接收频繁变化的 props

例如:

jsx 复制代码
<Chart data={data} />

但是:

text 复制代码
data
  ↓
父组件频繁变化
  ↓
Chart 重绘非常昂贵

如果:

text 复制代码
你控制 data 的状态更新

那么可以:

text 复制代码
useTransition

如果:

text 复制代码
data 是父组件传进来的
或者来自你不方便修改的 Hook

可以:

javascript 复制代码
const deferredData = useDeferredValue(data);

然后:

jsx 复制代码
<Chart data={deferredData} />

二十、典型场景三:文本编辑器 + 实时预览

例如:

text 复制代码
Markdown 编辑器
       ↓
用户快速输入
       ↓
左侧文本需要立即显示

       +

右侧 Markdown Preview
       ↓
复杂解析
       ↓
复杂渲染

方案一:状态由当前组件控制

text 复制代码
editorText
previewText

可以:

javascript 复制代码
setEditorText(nextText);

startTransition(() => {
    setPreviewText(nextText);
});

方案二:预览组件直接拿 editorText

javascript 复制代码
const deferredText = useDeferredValue(editorText);

<Preview text={deferredText} />

判断标准仍然是:

text 复制代码
我控制状态更新
    ↓
useTransition

我无法控制值的源头
    ↓
useDeferredValue

二十一、面试题 14:useTransition 和 React.memo 能不能一起用?

核心思路(一句话)

完全可以,而且它们解决的是两个不同层次的问题。

text 复制代码
React.memo
    ↓
减少"不必要的渲染"

useTransition
    ↓
调整"必要渲染的优先级"

举例

假设有:

text 复制代码
页面
 ├── SearchInput
 └── HugeList

可以:

text 复制代码
HugeList 内部 item
        ↓
React.memo
        ↓
如果 item props 没变化
        ↓
跳过重新渲染

同时:

text 复制代码
整个 HugeList 的查询变化
        ↓
useTransition
        ↓
把这次必要更新降为 Transition

因此:

text 复制代码
React.memo
解决:
"这个组件其实不用重新渲染"

useTransition
解决:
"这个组件确实要重新渲染,但现在不是最紧急的"

这是非常经典的面试回答。


二十二、面试题 15:useDeferredValue 和 memo 为什么经常需要配合?

核心思路(一句话)

因为"值保持旧值"和"旧值情况下跳过组件渲染"是两个独立动作。

完整过程:

text 复制代码
query 更新
   ↓
父组件重新渲染
   ↓
deferredQuery 仍是旧值
   ↓
SlowList props 仍然相同
   ↓
React.memo 判断 props 没变
   ↓
跳过 SlowList
   ↓
输入框完成响应
   ↓
后台更新 deferredQuery
   ↓
SlowList props 发生变化
   ↓
SlowList 真正渲染

二十三、面试题 16:useDeferredValue 有哪些坑?

坑一:误以为它就是延迟几百毫秒

错误:

text 复制代码
useDeferredValue
= 延迟 300 毫秒

正确:

text 复制代码
useDeferredValue
= 没有固定时间
= React 调度决定什么时候继续后台渲染

官方对此有明确说明。(React)


坑二:误以为它能降低计算量

它不会自动让:

javascript 复制代码
items.filter(...)

变得更快。

它主要解决的是:

text 复制代码
什么时候做

而不是:

text 复制代码
做这件事需要多少 CPU

所以如果你的算法复杂度本身很高,仍然要优化算法。

例如:

text 复制代码
O(n²)

不要指望:

text 复制代码
useTransition

把它变成:

text 复制代码
O(n)

二十四、坑三:没有 memo,可能达不到预期

例如:

javascript 复制代码
const deferredValue = useDeferredValue(value);

return <SlowComponent value={deferredValue} />;

如果 SlowComponent 没有合理的 memo 化,那么父组件重新渲染的时候,它仍可能重新执行。

所以对于典型场景:

text 复制代码
useDeferredValue
+
React.memo

通常是更完整的优化组合。React 官方也强调,memo 适合用于组件频繁以相同 props 重渲染且渲染成本较高的情况。(React)


二十五、坑四:对象在每次 render 中都重新创建

这是非常容易被忽略的高级问题。

错误:

javascript 复制代码
function App({ data }) {
    const deferredData = useDeferredValue({
        data,
    });

    return <Chart data={deferredData} />;
}

因为:

javascript 复制代码
{
    data
}

每次 render 都是一个新对象。

因此:

text 复制代码
Object.is(oldObject, newObject)
= false

React 会认为值变化了。

官方文档也特别提醒:传入 useDeferredValue 的对象最好来自 render 外部,或者保证引用稳定;如果在每次 render 中创建新对象,会造成额外后台重新渲染。(React)

更合理的做法:

javascript 复制代码
function App({ data }) {
    const deferredData = useDeferredValue(data);

    return <Chart data={deferredData} />;
}

或者提前保证对象引用稳定。


二十六、坑五:useDeferredValue 不等于"永远使用旧值"

它只是:

text 复制代码
当前这次紧急渲染
    ↓
先允许旧值

后台渲染
    ↓
尝试切换到新值

最终目标仍然是:

text 复制代码
让 deferredValue 跟上最新 value

也就是说它不是数据缓存,也不是手动维护另一份永久状态。


二十七、坑六:Suspense 场景下,体验可能更特殊

useDeferredValue 与 Suspense 是集成的。如果新的 deferred value 导致后台渲染进入 Suspense:

text 复制代码
最新数据尚未准备好
        ↓
后台渲染 suspend
        ↓
用户继续看到旧内容
        ↓
数据准备完成
        ↓
再显示新内容

这种模式对于:

text 复制代码
搜索结果
页面切换
图表
复杂数据展示

尤其有价值。

React 官方文档明确说明,useDeferredValue 与 Suspense 集成时,如果后台更新发生挂起,用户可以继续看到旧的 deferred 内容,而不是立即看到突兀的 loading fallback。(React)


二十八、面试题 17:useTransition 和 useDeferredValue 是不是"让任务在后台线程跑"?

核心思路(一句话)

不是,它们仍然依赖浏览器主线程上的 React 渲染机制,只是 React 获得了中断和重新调度渲染工作的能力。

错误说法:

text 复制代码
useTransition
→ 开启子线程

或者:

text 复制代码
useDeferredValue
→ 把计算放到后台线程

都不准确。

更准确:

text 复制代码
JavaScript 仍然运行在浏览器 JavaScript 执行环境
          ↓
React 并发渲染允许 render work 被调度
          ↓
可暂停
可中断
可重新开始
          ↓
避免低优先级渲染长期霸占交互响应

React 18 官方对 Concurrent React 的解释也是"可中断的渲染模型",而不是简单的多线程模型。(React)


二十九、面试题 18:useTransition 能不能让一个非常耗时的算法变快?

核心思路(一句话)

不能,它解决的是调度问题,不是算法复杂度问题。

例如:

javascript 复制代码
startTransition(() => {
    setResult(doVeryExpensiveCalculation(data));
});

如果:

javascript 复制代码
doVeryExpensiveCalculation()

本身就是一个巨大的同步 JavaScript 计算,那么它仍然可能阻塞主线程。

所以必须区分:

text 复制代码
问题一:
React 的更新优先级不合理
        ↓
useTransition / useDeferredValue

问题二:
算法复杂度过高
        ↓
优化算法

问题三:
DOM 节点太多
        ↓
虚拟列表 / 分页 / 分块渲染

问题四:
重复渲染太多
        ↓
React.memo / useMemo / useCallback

问题五:
网络请求过多
        ↓
防抖 / 节流 / 缓存 / 请求取消

这是面试中非常加分的"问题分类能力"。


三十、面试题 19:大型列表场景只用 useTransition 就够了吗?

核心思路(一句话)

通常不够,Transition 负责调度,虚拟列表负责减少真实渲染节点,两者解决的是不同问题。

例如:

text 复制代码
100000 条数据
      ↓
即使 transition
      ↓
最后还是要渲染大量 DOM

更好的架构:

text 复制代码
用户输入
   ↓
高优先级 input 更新

   +

搜索条件
   ↓
useTransition
   ↓
后台计算

   +

列表
   ↓
虚拟列表
   ↓
只渲染当前可见区域

推荐架构

text 复制代码
               用户输入
                  │
                  ▼
          ┌────────────────┐
          │   搜索输入框    │
          └────────────────┘
                  │
           高优先级更新
                  │
                  ▼
              queryState
                  │
                  ▼
          startTransition
                  │
                  ▼
             搜索结果状态
                  │
                  ▼
          useMemo / 算法优化
                  │
                  ▼
            虚拟列表组件
                  │
                  ▼
       只渲染可视区域中的节点

这通常比"只加一个 useTransition"更接近真实生产方案。


三十一、面试题 20:startTransition 和 useTransition 有什么区别?

核心思路(一句话)

两者都可以标记 Transition,区别主要是 useTransition 属于 Hook,可以额外得到 isPendingstartTransition 是独立 API。


useTransition

javascript 复制代码
const [isPending, startTransition] = useTransition();

适合:

text 复制代码
React 组件内部
+
需要 pending 状态

startTransition

javascript 复制代码
import { startTransition } from 'react';

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

适合:

text 复制代码
不在组件 Hook 上下文里
+
只需要标记 Transition

官方明确说明,startTransition 本身不提供 isPending;如果需要跟踪 Transition 是否正在进行,应使用 useTransition。(React)


三十二、面试题 21:startTransition 里面的函数是不是"以后再执行"?

核心思路(一句话)

不是,传给 startTransition 的函数会立即执行;只是函数里面产生的状态更新被标记成 Transition。

例如:

javascript 复制代码
startTransition(() => {
    console.log('立即执行');

    setSearchQuery('react');
});

执行顺序仍然是:

text 复制代码
startTransition()
   ↓
立即执行 callback
   ↓
调用 setSearchQuery
   ↓
这个 Update 被标记为 Transition
   ↓
React 后续调度渲染

React 官方对此有明确说明。(React)

所以:

text 复制代码
startTransition ≠ setTimeout

三十三、面试题 22:异步请求之后的 setState 能不能直接算 Transition?

核心思路(一句话)

需要特别注意 async 边界;当前 React 文档明确要求,在 await 之后的状态更新需要再次包裹 startTransition 才能保证被标记为 Transition。

官方当前文档明确说明了这一点。(React)

例如:

javascript 复制代码
startTransition(async () => {
    const result = await fetchData();

    startTransition(() => {
        setResult(result);
    });
});

面试中建议回答:

text 复制代码
同步更新:
startTransition 直接可以标记。

跨 await:
注意 React 版本和异步 Action 行为,
当前 React 文档要求 await 后的 setState 再进行 Transition 标记。

这样既准确,也避免把不同 React 版本的异步 Transition 行为混成一个模型。


三十四、面试题 23:如果 props 没变化和 props 确实变化,该怎么优化?

核心思路(一句话)

props 没变化重点解决"为什么还要渲染";props 变了重点解决"必须渲染时如何不阻塞用户"。


情况一:props 没变化

text 复制代码
父组件重新渲染
     ↓
子组件 props 实际没变
     ↓
React.memo
     ↓
跳过子组件重新渲染

情况二:props 确实变化

text 复制代码
props 确实变化
     ↓
组件确实必须重新渲染
     ↓
memo 无法阻止这次必要渲染
     ↓
如果它很慢
     ↓
useTransition / useDeferredValue
     ↓
把更新放到非紧急渲染路径

因此:

text 复制代码
memo
+
transition / deferred value

不是替代关系,而是可以组合。


三十五、最终对比表

对比维度 useTransition useDeferredValue
作用对象 状态更新 一个值
核心能力 标记更新为 Transition 产生值的延迟版本
是否主动 主动 被动
是否需要控制 setState 不需要
是否返回 pending 状态
适合 props 间接 很适合
适合搜索 很适合 也适合
适合图表 props 可以 很适合
是否固定延迟
是否防抖
是否节流
是否减少网络请求
是否减少算法复杂度
是否能与 memo 一起用 可以 非常常见
核心关键词 "更新优先级" "值允许滞后"

三十六、最关键的场景判断

场景一:我自己控制状态更新

javascript 复制代码
const [query, setQuery] = useState('');

然后:

javascript 复制代码
setQuery(nextValue);

导致大列表重渲染。

优先考虑:

javascript 复制代码
useTransition

模型:

text 复制代码
setInputValue
     ↓
立即更新

setSearchQuery
     ↓
startTransition
     ↓
后台渲染

场景二:值是父组件传来的

javascript 复制代码
function Chart({ data }) {
    // data 很重
}

我无法控制:

text 复制代码
data

什么时候更新。

可以:

javascript 复制代码
const deferredData = useDeferredValue(data);

然后:

javascript 复制代码
<Chart data={deferredData} />

场景三:子组件实际上没必要重渲染

先考虑:

text 复制代码
React.memo

场景四:数据量达到十万、百万级

不能只想着:

text 复制代码
useTransition

还要考虑:

text 复制代码
算法
+
虚拟列表
+
分页
+
缓存
+
memo
+
数据结构优化

三十七、整个知识点的架构图

text 复制代码
                    React 性能卡顿
                          │
                          ▼
                用户交互是否被阻塞?
                          │
            ┌─────────────┴─────────────┐
            │                           │
           是                           否
            │                           │
            ▼                           ▼
    找到高成本更新                可能无需优化
            │
            ▼
    是否存在不必要渲染?
            │
       ┌────┴────┐
       │         │
      是         否
       │         │
       ▼         ▼
 React.memo   必须渲染
 useMemo          │
 状态下沉         ▼
 虚拟列表     是否能控制 setState?
                  │
           ┌──────┴──────┐
           │             │
          能             不能
           │             │
           ▼             ▼
  useTransition    useDeferredValue
           │             │
           └──────┬──────┘
                  │
                  ▼
          Concurrent Rendering
                  │
                  ▼
      render 可暂停 / 中断 / 重启
                  │
                  ▼
       更紧急交互优先得到处理

三十八、原稿中需要纠正的关键错误

错误 1:"use default value"

应统一修正为:

text 复制代码
useDeferredValue

原稿实际描述的就是 React 的 useDeferredValue


错误 2:"延迟值,等浏览器不忙时再更新"

这个描述只能作为非常粗略的比喻。

更准确的是:

text 复制代码
当前渲染先保留旧值
+
React 在后台尝试用新值重新渲染
+
后台工作可以被更高优先级更新打断

而且:

text 复制代码
没有固定等待时间

(React)


错误 3:"避免阻塞主线程"

需要更严谨。

不是:

text 复制代码
React 把耗时工作移出主线程

而是:

text 复制代码
React 获得可中断的并发渲染能力
+
允许低优先级渲染被高优先级更新打断

(React)


错误 4:"useDeferredValue 就是一个值的副本"

这种说法容易误导。

它不是:

text 复制代码
复制一份 state

更准确:

text 复制代码
基于当前 value 得到一个可以滞后的 deferred value

本质上仍然由 React 的渲染与调度机制维护。


错误 5:"useTransition 控制状态更新时间"

也不够准确。

它不是:

text 复制代码
开发者指定什么时候执行

而是:

text 复制代码
告诉 React:
这次更新优先级没那么高,
允许后台渲染,
如果出现更紧急任务可以中断。

三十九、生产项目中的最佳实践

一个真实的大型搜索页面,我更推荐:

text 复制代码
                    用户输入
                       │
                       ▼
                 输入框状态
                       │
                       │ 高优先级
                       ▼
                立即更新 input
                       │
                       ├─────────────┐
                       │             │
                       ▼             ▼
                  搜索关键词      UI反馈
                       │
                       ▼
                useTransition
                       │
                       ▼
              后台计算搜索结果
                       │
                       ▼
             useMemo / 合理算法
                       │
                       ▼
                 React.memo
                       │
                       ▼
                   虚拟列表
                       │
                       ▼
               只渲染可视区域

如果还涉及网络请求:

text 复制代码
输入
 ↓
防抖
 ↓
请求取消 / 去重
 ↓
缓存
 ↓
获得数据
 ↓
Transition / deferred rendering
 ↓
UI 更新

也就是:

text 复制代码
网络性能问题
→ 防抖 / 缓存 / 取消请求

计算性能问题
→ 算法优化 / memo / useMemo

渲染调度问题
→ useTransition / useDeferredValue

DOM 数量问题
→ 虚拟列表

这才是比较完整的性能优化体系。


四十、面试满分答案

面试官问:

"请你介绍一下 useTransition 和 useDeferredValue,以及它们有什么区别?"

满分回答:

核心思路:

useTransitionuseDeferredValue 都是 React 并发渲染体系下用来解决"紧急交互和昂贵 UI 更新发生优先级冲突"的工具。它们不是让代码执行得更快,也不是启动新线程,而是让 React 能够把一部分渲染工作放到可中断、可重启的后台渲染路径中,从而保证用户交互优先。

useTransition 控制的是状态更新 。它返回 isPendingstartTransition,我可以把自己控制的某个状态更新包裹在 startTransition 中,告诉 React 这个更新不紧急。例如搜索场景里,输入框本身必须立即更新,但是根据输入进行大列表筛选可以放进 Transition。这样用户继续输入时,React 可以优先处理输入,而暂时中断正在进行的列表渲染。官方也明确说明 Transition 更新可以被更紧急的更新中断。(React)

useDeferredValue 控制的是值本身 。比如一个组件收到父组件传来的 data,我无法控制 data 从哪里更新,但是这个 data 驱动的图表渲染非常昂贵,这时可以通过 useDeferredValue(data) 得到一个允许滞后的版本。React 当前渲染可以继续使用旧值,然后在后台尝试使用新值重新渲染,如果过程中来了更紧急的更新,后台渲染可以被打断并使用最新值重新开始。它没有固定的延迟时间,也不是防抖或者节流。(React)

两者最大的区别可以总结成一句话:

text 复制代码
useTransition:
"我控制这个更新,我把这个更新标记成非紧急。"

useDeferredValue:
"我控制不了这个值什么时候变化,但我允许使用这个值的部分 UI 暂时落后。"

性能优化时还要注意,useTransitionuseDeferredValue 解决的是调度问题 ,而 React.memouseMemo、虚拟列表解决的是减少工作量的问题。如果组件本身根本不需要重新渲染,优先使用 memo 化;如果组件确实必须重新渲染但渲染很慢,再考虑 Transition 或 deferred value。

比如一个搜索大列表:

text 复制代码
输入框
→ 高优先级立即更新

搜索状态
→ useTransition

列表组件
→ React.memo

列表节点过多
→ 虚拟列表

搜索算法复杂
→ 算法优化

请求过于频繁
→ 防抖、缓存、请求取消

所以最终不是简单记住"该用哪个 Hook",而是先判断:

text 复制代码
这是不必要的渲染?
    → memo

这是必要但昂贵的更新?
    → Transition / deferred value

这是算法太慢?
    → 算法优化

这是 DOM 太多?
    → 虚拟列表

这是网络请求过多?
    → 防抖、缓存、请求取消

一句话收尾:

useTransition 是"给更新降优先级",useDeferredValue 是"允许一个值暂时落后",两者最终都是为了让高优先级用户交互不被昂贵渲染阻塞。


四十一、最后只背这 8 句话

text 复制代码
1. React 18 的核心能力之一是并发渲染,渲染过程可以被中断。

2. useTransition 控制"状态更新",useDeferredValue 控制"值"。

3. useTransition 适合我自己能控制 setState 的场景。

4. useDeferredValue 适合值来自 props 或其他地方、我无法控制更新源头的场景。

5. input 本身不能使用 Transition 控制,否则会影响输入响应性。

6. useDeferredValue 不是防抖,不是节流,也没有固定延迟。

7. memo 解决"不必要渲染",Transition 和 deferred value 解决"必要但昂贵的渲染如何不阻塞交互"。

8. useTransition / useDeferredValue 不会让算法更快,也不会自动减少网络请求。

这 8 句话基本覆盖了这道题最核心的面试考点。

补充一个现代 React 的面试加分点:当前 React 官方文档已经进入 React 19.x 文档体系,并进一步强调了 Actions、Suspense 以及 React Compiler 等能力;React Compiler 可以自动做与 memo 类似的组件记忆化优化,但它依然不能替代 useTransition / useDeferredValue 的"更新调度"职责 。(React)

后续复习这部分时,最值得继续往下深挖的是 "React Fiber + Lane + Scheduler + render/commit 两阶段 + Transition 如何触发可中断渲染",这会把你现在的"会用 Hook"提升到"真正理解 React 并发渲染原理"。

相关推荐
梦想的颜色2 小时前
【AI科普】AI 时代,纯 H5+CSS PK React & Vue:前端技术孰优孰劣深入剖析
ai·前端框架·大模型·vue·react·html5·vibecoding
liangshanbo121514 小时前
面试题:React 中 useReducer 和 useState 有什么区别?什么时候应该使用 useReducer?
react·usereducer
FungLeo1 天前
成为全栈·React 管理后台篇·后台骨架:布局、数据路由与分层守卫
react·管理后台·权限控制·react router·前端路由·成为全栈
秋秋小事1 天前
React hooks总览
react
西瓜太郎12344 天前
外层模型卡片与分组详情成功率不一致,应该怎么排查?
前端·typescript·go·软件工程·react
星辰徐哥4 天前
本地视频预览别只自己看:把Remotion动效项目发给客户远程验收
docker·ai·node.js·html·音视频·react·remotion
知兀5 天前
【前端】受控和非受控组件
前端·javascript·react
醉颜凉6 天前
React触摸事件全解析:掌握移动端交互开发的核心秘籍
react·前端开发·触摸事件·移动端交互·react事件
递归尽头是星辰6 天前
Java 智能体工程化:从 Spring AI 出发读懂 AgentScope
react·智能体·springai·javaai·agentscope