
一句话:
useTransition/useDeferredValue不减少工作量——它们改变的是「优先级」,让「昂贵但不紧急的更新」不阻塞「紧急交互(如输入回显)」,从而改善可交互性(INP)。
一、先明确它改变的是什么
❌ 「并发特性会让渲染更快」→ 错
总工作量没变(该算的还要算,该渲染的还要渲染)
✅ 「并发特性让紧急更新不被阻塞」
把工作分成「紧急」与「非紧急」,
非紧急的可以被中断、延后,让紧急的先完成。
| 手段 | 改变什么 |
|---|---|
memo / useMemo / 状态下移 |
减少工作量 |
| 虚拟滚动 | 减少工作量(DOM 数量) |
| 代码分割 | 减少首屏工作量 |
useTransition / useDeferredValue |
改变优先级(工作量不变) |
| Web Worker | 把工作移到别的线程 |
所以它们的定位是:
「已经无法再减少工作量,但用户抱怨卡」时的手段。
典型场景:输入驱动一个「必须渲染的大列表」。
二、useTransition 的四个适用场景
1) 输入驱动的昂贵渲染
function Search() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState<Item[]>([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setKeyword(value); // ⭐ 紧急:输入框立刻回显
startTransition(() => {
setResults(filterHugeList(value)); // ⭐ 非紧急:可以被打断
});
};
return (
<>
<input value={keyword} onChange={handleChange} />
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{results.map((r) => <Row key={r.id} item={r} />)}
</div>
</>
);
}
2) 切换 Tab / 路由
function Tabs({ tabs }: { tabs: Tab[] }) {
const [active, setActive] = useState(tabs[0]!.key);
const [isPending, startTransition] = useTransition();
return (
<>
<TabNav
tabs={tabs}
active={active}
onChange={(key) => startTransition(() => setActive(key))}
// ⭐ 点击「立刻」更新选中态(如果你把选中态也放进 transition,
// Tab 的选中高亮就会滞后 —— 通常应该只把「内容」放进 transition)
/>
<div style={{ opacity: isPending ? 0.6 : 1 }}>
<TabPanel tabKey={active} />
</div>
</>
);
}
// 更精确的写法:选中态「紧急」,内容「非紧急」
function Tabs2({ tabs }: { tabs: Tab[] }) {
const [active, setActive] = useState(tabs[0]!.key);
const [content, setContent] = useState(() => loadContent(tabs[0]!.key));
const [isPending, startTransition] = useTransition();
const handleChange = (key: string) => {
setActive(key); // ⭐ 紧急:高亮立刻切换
startTransition(() => {
setContent(loadContent(key)); // ⭐ 非紧急:内容可以稍后
});
};
// ...
}
3) 路由跳转
function useNavigateTransition() {
const navigate = useNavigate();
const [isPending, startTransition] = useTransition();
const go = useCallback((to: string) => {
startTransition(() => navigate(to)); // 内容渲染不阻塞「点击反馈」
}, [navigate]);
return { go, isPending };
}
4) 昂贵的派生状态更新
// 场景:用户拖动滑块,需要重新计算「一大块数据」
function Slider() {
const [value, setValue] = useState(50);
const [preview, setPreview] = useState(() => computePreview(50));
const [isPending, startTransition] = useTransition();
const handleChange = (v: number) => {
setValue(v); // 紧急:滑块位置立刻跟手
startTransition(() => {
setPreview(computePreview(v)); // 非紧急:预览可以滞后
});
};
return (
<>
<input type="range" value={value} onChange={(e) => handleChange(+e.target.value)} />
<Preview data={preview} dim={isPending} />
</>
);
}
三、useDeferredValue 的适用场景
// ① 值来自 props(改不了源头)——这是它不可替代的场景
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query); // ⭐ 只延迟「使用」
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);
const isStale = query !== deferredQuery; // 判断是否在显示旧结果
return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
{results.map((r) => <Row key={r.id} item={r} />)}
</div>
);
}
// ② 延迟「昂贵的子树」渲染(让其它部分先跟手)
function Page({ data }: { data: Data }) {
const deferredData = useDeferredValue(data);
return (
<>
<Header value={data.title} /> {/* 紧急:立刻用最新数据 */}
<Sidebar value={data.summary} /> {/* 紧急 */}
<ExpensiveChart data={deferredData} /> {/* 非紧急:用滞后数据(可被打断) */}
</>
);
}
// ③ 配合 Suspense 的「过渡加载」(避免「切换时闪 loading」)
function Detail({ id }: { id: string }) {
const deferredId = useDeferredValue(id);
// 当 id 变化时,先用 deferredId(旧值)继续显示内容
// → 新内容就绪后再切换 → 避免「立刻显示 loading」的闪烁
return (
<Suspense fallback={<Skeleton />}>
<Content id={deferredId} />
</Suspense>
);
}
四、与防抖/节流的区别与组合
| 维度 | 防抖(debounce) | useDeferredValue |
|---|---|---|
| 控制维度 | 时间 | 优先级 |
| 是否有固定延迟 | ✅(如 300ms) | ❌(由浏览器空闲决定) |
| 减少执行次数 | ✅(跳过中间输入) | ❌(每次都算,只是可被打断) |
| 适合 | 发请求 | 同步的昂贵渲染 |
| 是否「跟手」 | 有延迟感 | 更跟手(紧急部分立即响应) |
// ✅ 最佳组合:请求用防抖 + 渲染用 deferred
function Search() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState<Item[]>([]);
// ① 请求:防抖(减少网络请求次数)
const debouncedKeyword = useDebounce(keyword, 300);
useEffect(() => {
if (!debouncedKeyword) return;
let cancelled = false;
api.search(debouncedKeyword).then((data) => { if (!cancelled) setResults(data); });
return () => { cancelled = true; };
}, [debouncedKeyword]);
// ② 渲染:deferred(保持输入流畅)
const deferredResults = useDeferredValue(results);
const isStale = results !== deferredResults;
return (
<>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<ResultList results={deferredResults} dim={isStale} />
</>
);
}
一句话区分:
防抖 = 「等你停下来我再算」(减少次数)
deferred = 「你继续打字,我稍后补上」(不阻塞)
两者解决不同问题,可以叠加使用。
五、isPending 的 UI 设计原则
// ✅ 轻量提示:降低透明度
<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>
// ✅ 细进度条(顶部)
{isPending && <div className="top-progress-bar" />}
// ✅ 小 spinner(局部)
{isPending && <Spinner size="small" />}
// ❌ 全屏 loading 遮罩(会让用户觉得「更慢」)
{isPending && <FullScreenOverlay />}
三条原则:
① 「轻」——不要遮挡内容,不要用全屏遮罩
② 「局部」——只在「正在更新的那块区域」提示
③ 「不打断」——避免明显的位置跳动(否则看起来更卡)
⚠️ 因为 transition 更新通常很快就完成(几十~几百毫秒),
所以提示要「轻到几乎察觉不到」才合适。
六、什么时候它们无效
① 「紧急更新本身就很重」
如果输入框所在的组件必须渲染一个大列表,
那 setKeyword 本身就会触发长渲染 —— transition 帮不上忙。
解法:拆分(把大列表隔离到独立组件)+ memo + 虚拟滚动。
② 卡顿来自非 React 的原因
DOM 数量、布局抖动、网络等待、第三方库同步计算
→ 这些不属于 React 的渲染调度范围。
③ 用错了地方
把「输入受控值」「hover 反馈」「loading 状态」放进 transition
→ 输入会卡、反馈会迟。
④ 没有 createRoot(legacy 模式)
→ 并发特性不生效。
七、怎么度量收益
① 用 Chrome DevTools 的 Performance 面板看「交互延迟」
· 找到「输入」对应的长任务
· 优化前:一次按键触发的 JS 执行可能 100ms+(阻塞下一帧)
· 优化后:紧急更新很短,非紧急更新被拆开
② 用 Web Vitals 的 INP(Interaction to Next Paint)
· INP 衡量「用户交互到界面响应」的延迟
· transition 的直接收益就是「降低 INP」
③ 用 React DevTools Profiler 看「一次交互触发的渲染」
· 优化后,你会看到渲染被「分成了多次小片段」
(因为非紧急部分可以被打断)
⚠️ 注意:transition 不会让「总耗时」变短,所以不要用「总渲染时间」当指标。
正确的指标是「用户感知的响应延迟」。
八、常见坑
// ① 用 transition 包「受控输入的值」
startTransition(() => setInputValue(v)); // ❌ 输入卡顿
// ② 用 transition 包「loading 提示 / hover 反馈」
startTransition(() => setLoading(true)); // ❌ 用户看不到 loading
// ③ 认为 transition 能减少计算量
// → 它只是「改变优先级」
// ④ 用 transition 替代「减少渲染的工作」(应该先做后者)
// ⑤ 忘记 createRoot → 特性不生效
// ⑥ 用全屏遮罩展示 isPending(看起来更慢)
// ⑦ 没给「过渡中的内容」降低透明度(用户看不到「正在更新」)
// ⑧ 在 transition 里做副作用(如发请求)
startTransition(() => { api.post(data); }); // ❌ 副作用不该放这里
// ⑨ 把「切换选中态」也放进 transition(高亮滞后)
// ⑩ 期待 transition 解决「DOM 数量多」的问题(应该用虚拟滚动)
// ⑪ 用「总渲染耗时」衡量收益(应该看交互延迟/INP)
// ⑫ 在 transition 内部读「更新后的 state」(拿到的是当前快照)
startTransition(() => {
setList(filter(keyword)); // ⚠️ keyword 是当前渲染的闭包值
});
九、面试延伸
- 「
useTransition能提升性能吗?」
严格说它不提升「性能」但提升「响应性」——它不减少总工作量(该渲染的还要渲染),而是把更新分成「紧急」与「非紧急」,让非紧急的可以被中断、延后,从而让紧急更新(如输入回显、滑块位置)立即响应。所以正确的说法是「它改善可交互性(INP / 交互延迟)」,而不是「让页面渲染更快」。 -
「什么场景适合用
useTransition?」
四种:① 输入驱动昂贵渲染(搜索框过滤大列表——输入值紧急、列表非紧急);② Tab / 路由切换(选中态紧急、内容非紧急);③ 昂贵的派生计算(拖动滑块时重算预览);④ 路由跳转(点击反馈紧急、内容渲染非紧急)。共同特征是「有一个紧急的交互反馈 + 一个昂贵的后续工作」。 -
「
useTransition和useDeferredValue怎么选?」
判断标准是「你能不能控制「更新在哪里发生」」:① 能控制(你自己的setState)→ 用useTransition(还能拿到isPending);② 不能控制(值来自 props / 外部)→ 用useDeferredValue(它只延迟「使用」,不改更新逻辑)。此外useDeferredValue更适合「延迟渲染某个子树」或「配合 Suspense 避免闪 loading」。 -
「它和防抖有什么不同?」
防抖是「时间维度」的控制(延迟固定毫秒数),能减少执行次数(跳过中间输入)——适合发请求;useTransition/useDeferredValue是「优先级维度」的控制(让紧急的先完成),不减少执行次数——适合同步的昂贵渲染。两者定位不同,实践中常组合使用(请求防抖 + 渲染 deferred)。一句话:「防抖减少次数,transition 改变优先级」。 -
「
isPending应该怎么用?」
三条原则:① 轻——不要遮挡内容(用「降低透明度」或「细进度条」或「小 spinner」);② 局部——只在「正在更新的那块区域」提示;③ 不打断——避免明显的位置跳动。不要用全屏 loading 遮罩——因为 transition 的更新通常在几十到几百毫秒内完成,用重遮罩会让用户觉得「更慢」,效果适得其反。 -
「什么情况下
useTransition帮不上忙?」
四种:① 紧急更新本身就很重——比如输入框所在的组件必须渲染大列表,那么setKeyword自己就会触发长渲染;解法是「拆分组件 + memo + 虚拟滚动」;② 卡顿来自非 React 原因——DOM 数量、布局抖动、网络等待、第三方库的同步计算;③ 用错地方——把输入受控值、hover 反馈、loading 状态放进 transition;④ 没有用createRoot——legacy 模式下并发特性不生效。 -
「并发特性的收益怎么度量?」
不能用「总渲染耗时」(它不会变短)。正确的指标是「用户感知的响应延迟」:① INP(Interaction to Next Paint)——Web Vitals 的核心指标之一,衡量「交互到界面响应」的延迟,transition 的直接收益就体现在它上面;② Chrome Performance 面板里的长任务——优化后「单次按键触发的长任务」会被拆成多个片段;③ 主观体验——输入是否跟手、滑块是否跟手。总之要看「感知」而不是「总量」。 -
「什么时候优先用『减少工作量』而不是『改变优先级』?」
几乎总是优先「减少工作量」。因为:① 它从根本上变快(少渲染、少 DOM、少计算);② 它不依赖用户如何交互;③ 它不会引入「滞后感」。只有当「工作量确实无法再减少」(比如必须渲染这 1000 行、必须等这个数据)时,才用 transition 去「改善响应性」。正确顺序是「先减少工作量 → 再改变优先级 → 最后考虑 Web Worker / 虚拟滚动等结构性手段」。
一句话速记
useTransition/useDeferredValue改变的是「优先级」不是「工作量」,收益是可交互性(INP);useTransition用于「你能控制的setState」(配合isPending)、useDeferredValue用于「你控制不了的值(props)」;适合「紧急反馈 + 昂贵后续」的场景(搜索过滤、Tab 切换、滑块预览、路由跳转);不要包「输入受控值 / hover / loading」(会适得其反);与防抖的分工是「防抖减少次数(请求)、transition 改变优先级(渲染)」;isPending要轻量、局部、不打断(别用全屏遮罩);没有createRoot就不生效;度量看 INP 与交互延迟,而不是总渲染耗时。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!