React useTransition 和 useDeferredValue 面试题整理
一、先建立整套知识框架
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,可以额外得到 isPending;startTransition 是独立 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,以及它们有什么区别?"
满分回答:
核心思路:
useTransition 和 useDeferredValue 都是 React 并发渲染体系下用来解决"紧急交互和昂贵 UI 更新发生优先级冲突"的工具。它们不是让代码执行得更快,也不是启动新线程,而是让 React 能够把一部分渲染工作放到可中断、可重启的后台渲染路径中,从而保证用户交互优先。
useTransition 控制的是状态更新 。它返回 isPending 和 startTransition,我可以把自己控制的某个状态更新包裹在 startTransition 中,告诉 React 这个更新不紧急。例如搜索场景里,输入框本身必须立即更新,但是根据输入进行大列表筛选可以放进 Transition。这样用户继续输入时,React 可以优先处理输入,而暂时中断正在进行的列表渲染。官方也明确说明 Transition 更新可以被更紧急的更新中断。(React)
useDeferredValue 控制的是值本身 。比如一个组件收到父组件传来的 data,我无法控制 data 从哪里更新,但是这个 data 驱动的图表渲染非常昂贵,这时可以通过 useDeferredValue(data) 得到一个允许滞后的版本。React 当前渲染可以继续使用旧值,然后在后台尝试使用新值重新渲染,如果过程中来了更紧急的更新,后台渲染可以被打断并使用最新值重新开始。它没有固定的延迟时间,也不是防抖或者节流。(React)
两者最大的区别可以总结成一句话:
text
useTransition:
"我控制这个更新,我把这个更新标记成非紧急。"
useDeferredValue:
"我控制不了这个值什么时候变化,但我允许使用这个值的部分 UI 暂时落后。"
性能优化时还要注意,useTransition 和 useDeferredValue 解决的是调度问题 ,而 React.memo、useMemo、虚拟列表解决的是减少工作量的问题。如果组件本身根本不需要重新渲染,优先使用 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 并发渲染原理"。