
一句话:两个 Hook 都基于「并发渲染」——把「不紧急的更新」降优先级,让「紧急更新(如输入回显)」先完成;
useTransition用于你主动触发的状态更新,useDeferredValue用于被动接收的值。
一、背景:为什么要区分优先级
// ❌ 问题:一个「输入框 + 大列表过滤」的组件
function Search() {
const [keyword, setKeyword] = useState('');
const [list, setList] = useState(BIG_LIST);
// 每次输入都要过滤 10000 条数据 → 主线程被占满 → 输入卡顿
const filtered = list.filter((item) => item.name.includes(keyword));
return (
<>
setKeyword(e.target.value)} />
{filtered.map((it) =>
)}
>
);
}
// 用户感受:每敲一个字都要等几百毫秒,输入框「一顿一顿」的
// 根本问题:React 18 之前,所有更新都是「同步的、同优先级的」:
// setKeyword → 渲染开始 → 必须一口气渲染完(包括昂贵的列表)
// → 输入框的回显也被这个长渲染阻塞
并发渲染(Concurrent Rendering)提供的解法:
① 把更新分成「紧急」与「非紧急」
· 紧急(输入回显、hover 反馈)→ 必须立刻响应
· 非紧急(列表过滤、图表重算)→ 可以稍后、可以被中断
② 渲染过程可以【中断】,让浏览器先处理更高的优先级
③ React 会在「空闲时」继续未完成的工作,并且可以【丢弃过期的渲染】
⚠️ 注意:默认情况下 React 仍然把「普通 setState」当作紧急更新。
要区分优先级,必须显式使用这两个 Hook(或 startTransition)。
二、useTransition
const [isPending, startTransition] = useTransition();
// 用法:把「非紧急的更新」包进 startTransition
setKeyword(input); // ① 紧急:立即更新(输入框回显)
startTransition(() => {
setFilteredList(computeFiltered(input)); // ② 非紧急:可以被打断
});
// ✅ 改造上面的搜索组件
function Search() {
const [keyword, setKeyword] = useState('');
const [filtered, setFiltered] = useState(BIG_LIST);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent) => {
const value = e.target.value;
setKeyword(value); // 紧急更新:输入框立刻回显
startTransition(() => { // 非紧急更新:列表过滤可以延后
setFiltered(BIG_LIST.filter((it) => it.name.includes(value)));
});
};
return (
{/* 用 isPending 提示「正在更新」 */}
);
}
// 用户感受:输入立刻响应,列表稍后跟上(可以配一个轻微的 loading 态)
返回值:
| 返回值 | 含义 |
|---|---|
isPending |
是否有「正在进行的 transition(非紧急更新)」 |
startTransition |
把回调里的状态更新标记为非紧急 |
// 多个 transition 共存时,isPending 是「是否有任一 transition 在进行」
const [isPending, startTransition] = useTransition();
const switchTab = (tab: string) => {
startTransition(() => {
setActiveTab(tab); // 两个更新都是非紧急
setTabData(computeTabData(tab));
});
};
return (
<>
{isPending && } {/* 切换时显示轻量 loading */}
>
);
startTransition 也可以从 react 直接导入(不需要 Hook):
import { startTransition } from 'react';
// 用在「事件处理之外」的场景(如路由跳转、外部数据源回调)
router.push(path); // 紧急:导航要立即响应
startTransition(() => {
setPageContent(computeContent(path)); // 非紧急:内容计算可以延后
});
三、useDeferredValue
const deferredValue = useDeferredValue(value);
// 返回一个「滞后版本的 value」:紧急更新时它保持旧值,等空闲后再更新
// ✅ 用 useDeferredValue 改造(不改造「更新逻辑」,而是「延迟使用某个值」)
function Search() {
const [keyword, setKeyword] = useState('');
// ⭐ 不需要改 setKeyword 的写法,只在「使用」时用延迟版本
const deferredKeyword = useDeferredValue(keyword);
// 用滞后值做昂贵计算
const filtered = useMemo(
() => BIG_LIST.filter((it) => it.name.includes(deferredKeyword)),
[deferredKeyword]
);
const isStale = keyword !== deferredKeyword; // ⭐ 判断「正在显示旧结果」
return (
<>
setKeyword(e.target.value)} />
>
);
}
与 useTransition 的关键差异:
| 维度 | useTransition |
useDeferredValue |
|---|---|---|
| 作用对象 | 你触发的状态更新(包住 setState) |
传入的值(延迟它的使用) |
| 能否控制更新代码 | ✅ 需要把 setState 放进回调 |
❌ 不需要(改动更小) |
| 获取 pending 状态 | ✅ isPending |
❌ 需要自己比较(value !== deferred) |
| 适用场景 | 你能控制「更新在哪里发生」 | 值来自 props / 外部,你改不了源头 |
| 能否用于「非 state 的值」 | — | ✅(如 props、context 值) |
// useDeferredValue 的典型不可替代场景:值来自 props(无法改源头)
function List({ searchQuery }: { searchQuery: string }) {
// ❌ 不能用 useTransition:这里的渲染由父组件触发,我们控制不了 setState
// ✅ 用 useDeferredValue 延迟使用这个值
const deferredQuery = useDeferredValue(searchQuery);
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);
return {results.map((r) => - {r.name}
)}
;
}
// 另一个用途:延迟渲染「昂贵的子树」(相当于「选择性 hydration」的简化版)
function Page({ data }) {
const deferredData = useDeferredValue(data);
return (
<>
{/* 紧急:立刻用最新数据 */}
{/* 非紧急:用滞后数据 */}
>
);
}
四、与防抖/节流的区别
防抖/节流(debounce / throttle):
① 是「时间维度」的控制——延迟固定的毫秒数
② 与 React 的渲染机制无关(纯 JS 定时器)
③ 结果:输入停止后 X 毫秒才更新
④ 优点:能真正「减少」计算次数(跳过中间状态)
⑤ 缺点:有固定延迟(不够「跟手」),且需要管理定时器
useTransition / useDeferredValue:
① 是「优先级维度」的控制——让高优先级先完成
② 依赖 React 的并发渲染能力
③ 结果:紧急更新立刻生效,非紧急更新「尽快跟进」(通常比防抖快)
④ 优点:没有固定延迟,输入与结果可以「渐进对齐」
⑤ 缺点:不能减少总计算量(该算的还是会算),只是「不阻塞紧急更新」
| 维度 | 防抖 | useDeferredValue |
|---|---|---|
| 控制维度 | 时间 | 优先级 |
| 是否有固定延迟 | ✅(如 300ms) | ❌(由浏览器空闲时间决定) |
| 减少计算次数 | ✅(跳过中间输入) | ❌(每次都会算,但可被打断) |
| 适合 | 发请求(避免每次输入都请求) | 同步的昂贵渲染(本地过滤/重算) |
| 组合 | 两者可以一起用(请求用防抖、渲染用 deferred) |
// 组合使用:输入防抖发请求 + deferred 渲染
function Search() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
// ① 请求用防抖(减少网络请求)
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 (
<>
setKeyword(e.target.value)} />
{deferredResults.map((r) => )}
>
);
}
五、更多适用场景
// ① Tab 切换(大内容块)
function Tabs({ tabs }: { tabs: Tab[] }) {
const [active, setActive] = useState(tabs[0]!.key);
const [isPending, startTransition] = useTransition();
const switchTab = (key: string) => {
startTransition(() => setActive(key)); // 切换时旧的 tab 内容先留着
};
return (
<>
>
);
}
// ② 路由跳转(配合框架)
function useNavigateTransition() {
const navigate = useNavigate();
const [isPending, startTransition] = useTransition();
const go = (to: string) => {
startTransition(() => navigate(to)); // 跳转不阻塞当前页面的交互
};
return { go, isPending };
}
// ③ 延迟「昂贵的第三方组件」渲染(如编辑器、图表)
function Editor({ content }: { content: string }) {
const deferredContent = useDeferredValue(content);
return ;
}
六、常见坑
// ① 用 useTransition 包住「紧急更新」
startTransition(() => setInputValue(e.target.value)); // ❌ 输入回显变成非紧急 → 卡顿
// ✅ 只有「非紧急」的更新才放进 transition
// ② 把「需要立即反应的交互反馈」放进 transition
startTransition(() => setLoading(true)); // ❌ 用户会看不到 loading
startTransition(() => setHovered(true)); // ❌ hover 反馈必须紧急
// ③ 认为 transition 能「减少计算量」
// → 它只是「不阻塞紧急更新」,计算该做还是要做
// ④ 在 transition 内部依赖「更新后的 state」
startTransition(() => {
setList(filter(keyword)); // ⚠️ keyword 是当前渲染的闭包值,不是「更新后」的值
});
// ⑤ 用 transition 包住「受控输入」
startTransition(() => setText(value)); // ❌ 输入框会「卡字」
// ⑥ isPending 用得过重(如全屏 loading 遮罩)
// → 会让用户觉得「更慢」,应该用轻量的视觉提示(透明度、小 spinner)
// ⑦ 认为 useDeferredValue 会「跳过中间值」
// → 它只是「延迟使用」,中间值仍会渲染,只是优先级低
// ⑧ 用 useDeferredValue 替代「防抖发请求」
const d = useDeferredValue(keyword);
useEffect(() => { fetch(d); }, [d]); // ⚠️ 请求次数不会减少(只是时机稍晚)
// ✅ 请求应该用防抖,渲染用 deferred
// ⑨ 在 transition 里做副作用(不应该)
startTransition(() => { api.post(data); }); // ❌ 副作用应该同步执行
// ⑩ 认为「加了 transition 就一定流畅」
// → 如果「紧急更新本身」就很重(如输入框所在组件要渲染大列表),依然会卡
七、原理简述
背后的机制(简化):
① React 18 引入了「车道(Lane)」模型:
每个更新会被分配一个优先级车道(SyncLane / TransitionLane / ...)
② startTransition 把其中的更新标记为「非紧急」(TransitionLane)
③ 渲染时 React 按「优先级从高到低」处理:
高优先级车道可以先渲染、并【中断】正在进行的低优先级渲染
④ 低优先级渲染被打断后,React 会「稍后重试」(如果期间状态又变了,
会【丢弃】已作废的渲染,只算最新的)
⑤ 因此「紧急更新」总能立刻反映到屏幕上
useDeferredValue 的实现也是基于同一机制:
它相当于「用一个 transition 更新一个内部状态」,
并且会尝试「复用上一次的渲染结果」(所以实现上更轻量)
前提:并发特性需要
createRoot(React 18+ 的新入口,即ReactDOM.createRoot)。如果用旧的ReactDOM.render,这些特性不生效。
八、面试延伸
- 「
useTransition和useDeferredValue有什么区别?」
useTransition作用于「你主动触发的状态更新」——把setState包进startTransition回调,并返回isPending让你知道「后台更新是否在进行」;useDeferredValue作用于「传入的值」——返回该值的一个「滞后版本」,你在渲染昂贵部分时使用它。前者适合「你能控制更新代码」的场景,后者适合「值来自 props/外部、改不了源头」的场景。 -
「它们和防抖、节流有什么区别?」
防抖/节流是时间维度的控制(延迟固定毫秒数),能减少计算次数(跳过中间输入),适合发请求;useTransition/useDeferredValue是优先级维度的控制(让紧急更新先完成,非紧急更新可被打断),不减少计算量,适合同步的昂贵渲染。实践上两者常组合:请求用防抖,渲染用 deferred。 -
「为什么说它们依赖 React 18 的并发渲染?」
因为它们的能力来自「可中断的渲染 + 优先级调度」:React 18 用「车道模型」给每个更新分配优先级,渲染时可以中断低优先级的渲染、先处理高优先级,并在之后重试或丢弃过期的渲染。这套机制要求使用createRoot(React 18 的新入口),在旧的ReactDOM.render下这些特性不生效。 -
「什么更新应该放进
startTransition?」
所有「非紧急且可能耗时」的更新:大列表的过滤/排序结果、切换到内容很重的 Tab、路由跳转后的内容渲染、昂贵图表的数据更新、富文本/编辑器内容同步。不该放:输入框的受控值(setInputValue)、hover/focus 反馈、loading 状态、任何「用户期待立刻看到」的反馈——这些放进 transition 反而会造成卡顿。 -
「
isPending一般怎么用?」
用它做「轻量的过渡提示」,而不是「全屏 loading」。常见做法:① 把内容区域降低透明度(opacity: 0.6)表示「正在更新」;② 顶部显示一条细进度条;③ 显示一个小小的 spinner。关键是要轻——如果用全屏遮罩,用户会觉得更慢、更卡,与优化目标相反。 -
「
useDeferredValue是怎么实现的?」
它的原理与useTransition同源:React 内部会维护一个「延迟的值」,当传入的值变化时,它先用旧值继续渲染(保持界面稳定),然后在低优先级的更新中把延迟值更新为新值。实现上它相当于「用一个 transition 更新一个内部状态」,并且会尽量复用上一次的渲染结果(因此在某些场景比useTransition更轻量)。 -
「为什么不直接给列表加防抖?」
因为防抖解决的是「减少执行次数」,而渲染卡顿的根因是「一次渲染太长」——即使不防抖、每次都渲染,只要这次渲染可以被中断、不阻塞输入,用户感受就很好。而且防抖会引入固定延迟(300ms 后才更新结果),在快速输入时显得不跟手。两者定位不同:防抖减少「执行次数」,transition 降低「执行优先级」。 -
「用了这两个 Hook 页面就一定流畅吗?」
不一定。它们只解决「非紧急更新阻塞紧急更新」这一种问题。如果:① 紧急更新本身就很重(比如输入框所在组件必须渲染一个大列表),那依然会卡;② 卡顿来自别的原因(DOM 节点过多、布局抖动、网络等待),也帮不上忙;③ 过度的 transition 反而会让界面「滞后感」变重。所以仍应先用 Profiler 定位问题,再决定是否使用它们。
一句话速记
两者都基于 React 18 的并发渲染,把「非紧急更新」降优先级,让「输入回显」这类紧急更新不被阻塞;
useTransition包住你触发的setState(返回isPending),useDeferredValue延迟你接收到的值(适合 props 来源、改不了源头);不减少计算量(只是不阻塞紧急更新),与「防抖减少次数」定位不同,请求用防抖、渲染用 deferred;输入受控值、hover/loading 反馈不能放进 transition;isPending只做轻量提示(透明度、细进度条),别用全屏遮罩;需要createRoot才生效。



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