前言:为什么你的页面会"卡一下"?
你一定遇到过这种情形:
- 输入框一输入内容,页面卡顿 0.5 秒
- 切换 Tab 时,内容区域加载很久,UI"顿了一下"
- 搜索时,列表数据很多,界面直接"卡死两秒"
你可能会想:
"React 不是号称很快吗?怎么还卡?"
原因其实很简单:
React 会把所有状态更新一次性同步地渲染。你改了 state,它就赶紧渲染,不管这是不是紧急的。
所以如果你输入框触发的渲染很重(比如列表 5000 条),那输入就会卡。
为了给开发者更细粒度的控制------
React 提供了 useTransition。
它的核心目的就一句话:
让"不着急的更新,靠边站",优先保证"着急的更新"不被卡住。
这个理念是 React18 之后"并发特性"的核心之一。
第一章:useTransition 到底是什么?
1. 它不是加速器
它不会让渲染更快,也不会减少计算量。
2. 它是"任务优先级调度器"
把更新分成两类:
| 类型 | 示例 | 特点 |
|---|---|---|
| 🍎 Urgent(紧急更新) | 输入框输入、用户点击按钮、切换输入法 | 必须马上更新,否则卡顿 |
| 🍉 Transition(过渡更新) | 过滤列表、加载新界面、切换大视图,长列表重新渲染 | 可以延迟,不急 |
用法:
scss
const [isPending, startTransition] = useTransition()
startTransition(() => {
// 这里面的更新会被标记为"非紧急"
})
因此,React 会:
- 先处理紧急更新(保持 UI 流畅)
- 再"慢慢处理"这些不紧急的渲染
这就是 "不卡顿" 的根本原因。
🧪 第二章:极其直观的小白示例:为什么不卡?
假设你做一个搜索过滤:
ini
<input value={text} onChange={e => setText(e.target.value)} />
<List data={filterBigList(text)} />
filterBigList(text) 特别耗时
(比如几千条数据)
那么每次输入文字,都会:
- 更新 text
- filter 大数据列表
- 重新渲染大列表
结果就是:
- 输入法卡
- 页面卡
- 渲染跟不上用户输入
现在用 useTransition:
js
const [isPending, startTransition] = useTransition();
const handleChange = e => {
const v = e.target.value;
setText(v); // 紧急更新,输入框动作必须马上反映
startTransition(() => {
setFiltered(filterBigList(v)); // 不紧急
});
};
结果:
- 输入框立刻更新(流畅)
- filter 的计算被安排在低优先级,不阻塞 UI
- 列表先显示旧的,等计算完再显示新的
⚡ 第三章:为什么 useTransition 能分类更新?
因为 React18 有了 可中断渲染(interruptible rendering) 。
以前 React 是同步渲染:
"你让我渲染 5000 条?那我开始做,等我做完才能响应用户输入。"
现在是:
"我先渲染你这一键的输入(紧急),至于你过滤列表那玩意儿,我稍后再弄,不着急。"
渲染任务可被提前打断,这是并发特性的核心。
你按键的时候,React 认为"输入响应优先于渲染大列表",所以它会:
- 停 → 抛弃当前非紧急渲染
- 先做 → 输入框的 UI 更新
- 再做 → 非紧急渲染
这就是 useTransition 的魔法来源。
🧩 第四章:useTransition 的详细用法与 API 解析
4.1 基本使用
scss
const [isPending, startTransition] = useTransition();
🥝 startTransition(fn)
把 fn 内的状态更新变成非紧急。
🍋 isPending
表示"非紧急更新正在进行中"。
常用来显示 loading:
css
{isPending && <span>加载中...</span>}
4.2 示例完整版
js
function SearchPage() {
const [query, setQuery] = useState('');
const [list, setList] = useState(bigList);
const [isPending, startTransition] = useTransition();
const handleChange = e => {
const v = e.target.value;
setQuery(v); // 同步更新输入框
startTransition(() => {
const filtered = bigList.filter(item => item.includes(v));
setList(filtered); // 异步、延迟、不紧急
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>加载中...</p>}
<List data={list} />
</>
);
}
🧬 第五章:三种情况下必须用 useTransition
你只要看到下面三种情况,就应该考虑 useTransition。
5.1 情况一:输入框导致页面卡顿(最常见)
场景:输入框 → 触发大量渲染 → 输入卡顿
解决:用 useTransition 把重渲染丢到低优先级。
5.2 情况二:Tab 切换导致大页面渲染
比如切换 Tab 会渲染一个很大的组件:
scss
startTransition(() => {
setSelectedTab(tab)
})
你会发现 UI 流畅得像没事一样。
5.3 情况三:长列表重新渲染
长列表 + 过滤、切换排序、切换视图布局
这些都是非紧急更新 → 必用 useTransition。
🛑 第六章:useTransition 不适用的地方(容易踩坑)
有些地方你绝不能用它!
❌ 6.1 表单真实值不能放在 transition 内更新
错误:
scss
startTransition(() => {
setInputValue(x)
})
结果:
- 输入会延迟
- React 故意"慢慢更新"
正确:
scss
setInputValue(x)
❌ 6.2 不能用于必须立即更新的 UI
比如:
- 提交表单结果
- 弹出 Modal
- 提示错误信息
这些都属于"紧急更新",不能延迟。
🔍 第七章:深入原理(可当面试题)
面试官常问:
❓ useTransition 是如何实现"低优先级渲染"的?
核心逻辑是:
- startTransition 内的更新会被标记为
transition lane - React 在调度更新时,会根据 Lane 优先级调度
- transition lane(低优先级)渲染时,可以被更高优先级任务中断
- 新任务进来后,低优先级渲染被丢弃
- 等紧急任务完成后,再继续低优先级任务
也就是:
任务优先级 + 可中断渲染 + Lane 模型 = useTransition
❓ useTransition 和 useDeferredValue 差别?
非常高频面试题!
| Hook | 用途 |
|---|---|
| useTransition | 把一段更新标记成低优先级 |
| useDeferredValue | 延迟一个值,不影响其余组件 |
例子:
- 搜索框:用 useDeferredValue
- 点击 Tab 切换大页面:用 useTransition
- 渲染大列表:都能用,但一般 prefer Transition
💥 第八章:你见过最直观的示例:没有 useTransition vs 有 useTransition
🎮 没用 useTransition
你快速打字:
- 输入框卡卡卡
- CPU 占满
- 输入光标跳动卡顿
🎠 用了 useTransition
你快速打字:
- 输入框始终流畅
- 列表稍后加载
- UI 完全不卡
这就是 优先级调度 的威力。
🎯 第九章:useTransition 的最佳实践(大厂常用写法)
✔ 输入框里永远只放紧急更新
✔ 大计算/大渲染一定放 transition
✔ isPending 做 loading 占位
✔ 列表多时,可搭配虚拟列表(react-window)
✔ 不要过度使用,否则 UI 不同步反而奇怪
🏁 结尾总结(最面试的那一句)
如果面试官问你:
React18 的 useTransition 有什么作用?
你可以这样答:
它提供了一种把"不紧急渲染"放到低优先级执行的方式,依赖 React18 的并发特性和可中断渲染机制,通过 startTransition 包裹的更新可以被紧急任务打断,从而保证用户交互(如输入、点击)保持流畅。
一句话:
useTransition 能让 UI 保持流畅,是因为 React18 引入了并发更新机制,允许状态更新根据优先级调度。被 startTransition 包裹的更新属于低优先级,渲染可以被中断、丢弃或延后执行,从而不阻塞高优先级的用户输入等交互任务。