本文介绍React Hook中的useDeferredValue
概述
useDeferredValue 是 React 18 引入的一个 Hook,主要用于优化渲染过程中的性能,特别是当某些组件渲染很慢时。它可以让 React 将高优先级的任务(如用户输入)优先渲染,延迟低优先级的更新(如复杂计算或大型列表渲染)。useDeferredValue 适用于那些需要在用户交互时保持响应速度,但又有一些部分可以稍后更新的场景。
useDeferredValue 使用说明(给未来的自己)
1. 它到底是什么?
useDeferredValue(value) 会返回一个 “延迟更新版的 value”。
value:你传进去的最新值(高优先级)- 返回值
deferredValue:React 可能会 暂时保持旧值,等主线程不忙了再更新到最新值(低优先级)
记忆句:同一个值分身成“实时值”和“延迟值”。
2. 它解决什么问题?
当 UI 中存在两类更新:
- 必须立刻响应(比如输入框打字、按钮点击、计数器数字)
- 很昂贵但不那么急(比如大型列表过滤、图表计算、复杂组件渲染)
如果不用它:贵的那部分会抢占主线程,导致“必须立刻响应”的体验变卡。
useDeferredValue 的目标:
优先保证用户交互丝滑,把昂贵渲染延后执行。
3. 它的核心机制(你一定要记住)
React 在一次更新里,可能会“先更新一部分 UI”,另一部分“晚点再补上”。
- 使用
counter的地方:高优先级,尽快更新 - 使用
deferredCounter的地方:低优先级,可能延迟更新
所以你看到的现象经常是:
✅ 页面上的数字/输入框立刻变 ⏳ 旁边的昂贵组件过一会儿才跟上(短暂“滞后”)
记忆句:用户看到的“关键反馈”优先,耗时结果稍后补齐。
4. 最基础用法
const [value, setValue] = useState("");
const deferredValue = useDeferredValue(value);
// 轻的 UI 用 value(立刻变)
// 重的 UI 用 deferredValue(可以慢点)
关键策略:
把“昂贵渲染”的输入换成 deferredValue。
5. 例子 1:输入框不卡顿(最典型)
场景
你有个搜索框,下面是一个很大的列表,过滤/渲染很慢。 你希望:打字永远不卡,列表可以慢半拍更新。
✅ 推荐写法
function App() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
// 假设这里很贵:过滤 1w 条、排序、分组等
return bigList.filter(item => item.includes(deferredQuery));
}, [deferredQuery]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ResultList items={results} />
</>
);
}
要理解的点
- 输入框使用
query:每次键入立刻更新,不卡 - 列表用
deferredQuery:React 可能先让输入框更新,再在空闲时更新列表
✅ 用户体验:打字丝滑 ⏳ 列表可能慢一点跟上(但更舒服)
代码示例
App.tsx(主组件)import { useState, useDeferredValue } from "react"; import Expensive from "./components/expensive"; export default function App() { const [counter, setCounter] = useState(0); const deferred = useDeferredValue(counter); // 使用 useDeferredValue function __clickHanler() { setCounter(counter + 1); // 增加 counter 值 } return ( <div className="p-4"> <div className="flex items-center"> <Expensive counter={deferred} /> {/* 延迟更新 */} <div className="flex-1 text-center"> {counter} {/* 显示当前的 counter */} <div>高优先级任务</div> </div> </div> <div className="text-right mt-8"> <button className="button" onClick={__clickHanler}>counter++</button> </div> </div> ); }
Expensive.tsx(昂贵组件)import { memo } from "react"; import SlowItem from "./slow-item"; const Expensive = ({ counter }: { counter: number }) => { let items = []; for (let i = 0; i < 200; i++) { items.push( <SlowItem key={i} counter={counter} /> ); } return ( <div className="text-red-500 dark:text-red-400 flex-1"> <div className="flex-1 text-center"> {counter} {/* 显示 counter */} <div>耗时任务</div> </div> <ul className="h-32 hidden">{items}</ul> {/* 渲染 SlowItem 列表 */} </div> ); }; export default memo(Expensive); // 使用 memo 包裹
SlowItem.tsx(慢组件)export default function SlowItem({ counter }: { counter: number }) { let startTime = performance.now(); while (performance.now() - startTime < 1) { // 每个项目的延迟时间为1ms,以模拟极慢的代码 console.log(performance.now() - startTime); } return <li>{counter}</li>; // 返回 li 元素 }
解答问题:
1. 为什么
Expensive组件要用memo包裹?有什么用?不用memo可以吗?
memo是 React 提供的一个高阶组件,用于 缓存组件的渲染结果,避免不必要的重复渲染。
memo的作用:
- 通过
memo包裹,React 会记住组件的上次渲染结果,仅在props发生变化时才重新渲染。- 在你的代码中,
Expensive组件需要渲染一个包含 200 个SlowItem的列表。每次counter改变时,整个Expensive组件都会重新渲染。使用memo可以避免当counter未发生变化时不必要的重新渲染。- 不使用
memo会怎么样:
- 如果不使用
memo,每次counter改变时,Expensive会重新渲染,并且会重复执行SlowItem组件中的昂贵操作(如while循环)。- 如果你希望在
counter没有改变时,避免重新渲染整个Expensive组件,那么必须使用memo。- 是否可以不使用
memo:
- 可以不使用
memo,但是这样会导致每次counter更新时,Expensive组件都重新渲染,并且重新计算所有的SlowItem。这种做法会影响性能,尤其是在列表项很多或计算很慢时。
2.
SlowItem的while循环有什么用?不会导致程序卡死吗?
SlowItem中的while循环是用来模拟性能开销非常大的操作,目的是让这个组件的渲染变得很慢。
while循环的作用:
- 该循环使得
SlowItem组件的每次渲染都花费 1 毫秒,这会导致每个SlowItem渲染时卡住,模拟了一个非常低效的操作。在真实项目中,这可能是一些复杂的计算或长时间的 DOM 操作。- 不会导致程序卡死吗?
- 不会卡死,因为这个
while循环的延迟时间很短(1 毫秒)。浏览器会在事件循环中处理这部分,保证界面的响应性。- 然而,这种方式会 阻塞主线程,导致页面变得不流畅,尤其是在大量组件同时渲染时,页面渲染会变得很慢。因此,这种方法不推荐在生产环境中使用,但在这里它用于模拟性能瓶颈。
3. 明明
Expensive组件中的items是由SlowItem构成的,而SlowItem又返回了li,为什么页面上(也就是App.tsx中)没有显示出这些li?这是因为你在
Expensive组件中将items渲染在了一个ul元素中,而这个ul被设置了hidden类:<ul className="h-32 hidden">{items}</ul>
hidden类的作用:
hidden会让ul元素及其内容完全不可见。即使SlowItem渲染了li元素,它们也不会显示在页面上,因为ul被隐藏了。解决办法:
你可以移除
hidden类,或者使用block类来确保ul元素是可见的:<ul className="h-32">{items}</ul>
另外的一些疑问
1. 关于
SlowItem中的while循环:为什么刷新页面时while中的 log 会输出非常非常多次?首先,
SlowItem组件的while循环的目的是模拟一个非常慢的操作,它会阻塞主线程,直到performance.now()的差值超过 1 毫秒。这个循环虽然很短(1 毫秒),但它会阻塞主线程,导致每个SlowItem渲染时卡住。为什么
while中的 log 会输出非常多次?
SlowItem组件每渲染一次时,while循环就会执行,console.log(performance.now() - startTime)每次都会打印。由于你渲染了 200 个SlowItem组件,每个SlowItem都会执行这个阻塞操作。- 刷新页面时,因为浏览器要重新渲染所有的内容,
SlowItem组件会被重新渲染 200 次,每次都触发while循环,所以你会看到大量的console.log输出。解决方法:
- 这是为了模拟性能瓶颈,不推荐在生产环境中使用。
- 如果你希望测试这个延迟效果,可以在
while循环外部增加一个条件来限制输出次数,比如只在特定条件下打印 log。
2. 当我把
counter而不是deferred传入Expensive组件时,会不会引起性能问题?为什么?是的,直接传入
counter而不是deferred可能会引起性能问题,原因如下:
- 没有延迟:
counter是实时值,任何更新都会导致整个Expensive组件重新渲染。由于Expensive组件渲染了 200 个SlowItem,这意味着每次counter更新时,200 个SlowItem会被重新渲染,并且每个SlowItem都会执行阻塞的while循环。这会导致 UI 卡顿,影响用户体验。- 通过
useDeferredValue的优化:
deferred是经过延迟处理的值,这意味着Expensive组件可以在主线程处理完高优先级任务(如输入更新)后,再处理低优先级任务(如重新渲染昂贵的列表)。deferred更新会慢一点,但不会阻塞主线程,所以不会立即导致昂贵组件的渲染。- 使用
deferred作为counter的值可以避免每次更新都重新渲染整个Expensive组件及其嵌套的 200 个SlowItem。总结:直接传入
counter会导致性能问题,因为它每次更新都会触发昂贵的组件重新渲染,而 使用deferred可以减轻性能压力,延迟渲染昂贵的组件。
3.
const deferredQuery = useDeferredValue(query); const isStale = deferredQuery !== query;— 一开始deferredQuery会落后于query,那么当deferredQuery变成query的值时,页面会重新渲染吗?是的,当
deferredQuery更新并最终与query相同的时候,页面会重新渲染。解析:
- 初始状态:当你调用
useDeferredValue(query)时,deferredQuery会在初始时落后于query的值。也就是说,如果query在更新时,deferredQuery会稍微延迟更新,允许 React 优先处理其他任务(比如用户输入的即时反馈)。deferredQuery变成query的值时的情况:
- 当
deferredQuery与query值相同后,表示延迟更新已完成。- 由于
isStale是通过比较deferredQuery !== query来得出结果,因此当deferredQuery更新为query的值时,isStale将变为false,意味着延迟的值已达到最新状态。- 页面的重新渲染:
- 会重新渲染。因为 React 会重新计算组件的状态或重新渲染,尤其是
isStale的状态变化会导致依赖isStale或deferredQuery的部分重新渲染。小总结:
deferredQuery初始时落后于query,但当它和query一致时,React 会触发重新渲染。
4.
useDeferredValue一般是不是可以接受任何类型的值?然后将它传递到一个组件中,因为 React 组件是由数据驱动的,所以那个组件可能只需要一个值来驱动。是的,
useDeferredValue可以接受任何类型的值,并且它将这种值传递给组件,以便 React 根据这个值来驱动组件的渲染。解释:
useDeferredValue并不限制值的类型,它可以接受任何类型的值(例如:字符串、数字、对象、数组等)。useDeferredValue的工作方式是:它会将传入的值延迟更新,直到 React 认为可以安全地进行低优先级更新,这有助于避免昂贵的更新阻塞 UI 渲染。示例:
假设你有一个数组作为
useDeferredValue的输入:const [list, setList] = useState([]); const deferredList = useDeferredValue(list); // 在组件中传递 <ExpensiveList list={deferredList} />这个列表(
list)可以是任何类型的数据,React 会延迟更新它,直到有足够的空闲时间。总结:
- 是的,
useDeferredValue可以接受任何类型的值。- 通过这个机制,你可以确保传递给组件的数据在更新时能够以延迟的方式进行,从而不影响用户界面响应。
总结:
while循环的日志输出多次:因为每个SlowItem渲染时都会执行while循环,模拟了一个性能瓶颈,导致大量日志输出。- 直接传递
counter到Expensive会导致性能问题:因为每次更新counter,都会重新渲染整个Expensive组件及其子组件,导致性能下降。deferredQuery与query相同后会重新渲染:当deferredQuery更新为query的值时,isStale状态会改变,React 会触发重新渲染。useDeferredValue可以接受任何类型的值:它会将这个值延迟更新,传递给需要它的组件,帮助减少不必要的渲染开销。总结:
useDeferredValue的作用:优化用户交互和昂贵渲染之间的优先级,让高优先级任务(如用户输入、计数器更新)能立刻响应,低优先级的任务(如大型列表渲染)可以延迟执行。memo的作用:用于缓存组件渲染结果,避免重复渲染相同的组件,提升性能。while循环:用于模拟耗时操作,可能会导致 UI 阻塞,因此应谨慎使用。hidden类:阻止ul中的li显示。