本文介绍React Hook中的useDeferredValue

概述

useDeferredValue 是 React 18 引入的一个 Hook,主要用于优化渲染过程中的性能,特别是当某些组件渲染很慢时。它可以让 React 将高优先级的任务(如用户输入)优先渲染,延迟低优先级的更新(如复杂计算或大型列表渲染)。useDeferredValue 适用于那些需要在用户交互时保持响应速度,但又有一些部分可以稍后更新的场景。

useDeferredValue 使用说明(给未来的自己)

1. 它到底是什么?

useDeferredValue(value) 会返回一个 “延迟更新版的 value”

  • value:你传进去的最新值(高优先级)
  • 返回值 deferredValue:React 可能会 暂时保持旧值,等主线程不忙了再更新到最新值(低优先级)

记忆句:同一个值分身成“实时值”和“延迟值”


2. 它解决什么问题?

当 UI 中存在两类更新:

  1. 必须立刻响应(比如输入框打字、按钮点击、计数器数字)
  2. 很昂贵但不那么急(比如大型列表过滤、图表计算、复杂组件渲染)

如果不用它:贵的那部分会抢占主线程,导致“必须立刻响应”的体验变卡。

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. SlowItemwhile 循环有什么用?不会导致程序卡死吗?

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 的值时的情况
    • deferredQueryquery 值相同后,表示延迟更新已完成。
    • 由于 isStale 是通过比较 deferredQuery !== query 来得出结果,因此当 deferredQuery 更新为 query 的值时,isStale 将变为 false,意味着延迟的值已达到最新状态。
  • 页面的重新渲染
    • 会重新渲染。因为 React 会重新计算组件的状态或重新渲染,尤其是 isStale 的状态变化会导致依赖 isStaledeferredQuery 的部分重新渲染。

小总结:

  • 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 可以接受任何类型的值。
  • 通过这个机制,你可以确保传递给组件的数据在更新时能够以延迟的方式进行,从而不影响用户界面响应。

总结:

  1. while 循环的日志输出多次:因为每个 SlowItem 渲染时都会执行 while 循环,模拟了一个性能瓶颈,导致大量日志输出。
  2. 直接传递 counterExpensive 会导致性能问题:因为每次更新 counter,都会重新渲染整个 Expensive 组件及其子组件,导致性能下降。
  3. deferredQueryquery 相同后会重新渲染:当 deferredQuery 更新为 query 的值时,isStale 状态会改变,React 会触发重新渲染。
  4. useDeferredValue 可以接受任何类型的值:它会将这个值延迟更新,传递给需要它的组件,帮助减少不必要的渲染开销。

总结:

  • useDeferredValue 的作用:优化用户交互和昂贵渲染之间的优先级,让高优先级任务(如用户输入、计数器更新)能立刻响应,低优先级的任务(如大型列表渲染)可以延迟执行。
  • memo 的作用:用于缓存组件渲染结果,避免重复渲染相同的组件,提升性能。
  • while 循环:用于模拟耗时操作,可能会导致 UI 阻塞,因此应谨慎使用。
  • hidden:阻止 ul 中的 li 显示。