Hook18.0+

useDeferredValue

延迟更新 UI 的某些部分,让 React 在并发渲染中保持界面响应性

核心概述

痛点: 派生状态导致的渲染阻塞

在 React 应用中,经常会遇到这种场景:某个值(如 props 或 state)变化时, 需要重新渲染大量组件或执行复杂计算。即使输入变化很快, 这些派生的 UI 也会频繁更新,导致界面卡顿。

  • 搜索场景: 用户快速输入时,每次输入都会触发搜索列表重新渲染, 即使输入框本身应该保持响应
  • 图表场景: 数据频繁更新时,复杂图表的重绘会阻塞界面, 即使控制按钮需要立即响应
  • 列表场景: 过滤条件变化时,大型列表的过滤和渲染会延迟界面更新

解决方案: 延迟值更新

useDeferredValue 是 React 18 并发渲染特性的一部分。 它接受一个值,返回一个"延迟版本"。当原值频繁变化时, React 会优先处理更紧急的更新(如用户输入),延迟处理使用该值的 UI 部分。

  • 优先级分级: 区分紧急更新(输入框、按钮反馈)和非紧急更新(搜索结果、图表)
  • 延迟渲染: 派生的 UI 部分会在浏览器空闲时更新,不会阻塞用户交互
  • 可中断渲染: 如果原值继续变化,React 可能放弃旧的延迟渲染,直接渲染最新值
  • 保持引用: 在原值未变化时,返回相同的引用(使用 Object.is 比较)

适用场景

  • 搜索/过滤: 延迟搜索结果的渲染,保持输入框响应
  • 数据可视化: 延迟图表重绘,保持控制按钮可用
  • 大型列表: 延迟列表渲染,保持导航栏响应
  • 从 props 派生的复杂 UI: 父组件频繁更新时,延迟子组件渲染

💡 心智模型

将 useDeferredValue 想象成"稍后处理的服务窗口":

  • VIP 通道(紧急更新): 用户打字、点击等直接交互走 VIP 通道, 立即处理并显示
  • 普通窗口(延迟值): 搜索结果、图表等派生 UI 走普通窗口, 等待 VIP 处理完再处理
  • 可跳过机制: 如果 VIP 通道有新任务,普通窗口正在处理的旧任务可能被放弃, 直接处理最新的值
  • 相同值优化: 如果传入的值没变(Object.is 相等), 直接返回上次的值,不触发重新渲染

关键: useDeferredValue 不是"节流/防抖", 而是"优先级管理"。它不会延迟值的变化,而是延迟使用该值的 UI 渲染。

技术规格

类型签名

TypeScript
function useDeferredValue<T>(value: T): T

参数说明

参数类型说明
valueT要延迟的值,可以是任何类型(字符串、数字、对象、数组等)

返回值

返回一个与输入值类型相同的值,但更新时机被"延迟":

  • 首次渲染: 返回传入的 value
  • 后续渲染: 如果 value 变化,React 可能在当前渲染中立即返回旧值, 待紧急更新处理完后再重新渲染使用新值
  • 值未变化: 如果 value 与上次相同(Object.is 相等), 返回相同的引用,不触发依赖该值的组件重新渲染

运行机制

useDeferredValue 基于 React 18 的并发渲染特性,底层机制如下:

  • 优先级标记: 使用延迟值的组件被标记为低优先级更新
  • 时间切片: React 将渲染工作分解成小单元, 在每个时间切片后检查是否有更高优先级的更新
  • 可中断渲染: 如果在延迟渲染过程中有新的紧急更新(如用户输入), React 会放弃当前的延迟渲染,处理紧急更新后再重新开始
  • 值比较: 使用 Object.is 比较新旧值, 如果值未变化,复用上次的值,跳过重新渲染

注意: useDeferredValue 需要 React 18+ 和支持并发渲染的渲染器 (如 ReactDOM 18+ 的 createRoot)。使用旧版 ReactDOM.render() 不会启用并发特性。

实战演练

示例 1: 搜索输入框(基础用法)

最常见的场景:用户快速输入时,输入框立即响应,搜索结果延迟更新。

TypeScript
import { useState, useDeferredValue, useMemo } from 'react';

function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');

  // ✅ 延迟搜索查询,输入框可以立即响应
  const deferredQuery = useDeferredValue(query);

  // ✅ 基于延迟值过滤,不会阻塞输入
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [items, deferredQuery]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
        className="border p-2 rounded"
      />
      <ul className="mt-4 space-y-1">
        {filteredItems.map(item => (
          <li key={item} className="text-sm">{item}</li>
        ))}
      </ul>
    </div>
  );
}

效果: 用户快速打字时,输入框立即响应,即使 items 数组很大(如 10,000 条), 也不会感到卡顿。搜索结果会在浏览器空闲时更新。

示例 2: 数据可视化仪表板(生产级)

数据频繁更新时,复杂图表的重绘会阻塞界面。使用延迟值保持控制按钮响应。

TypeScript
import { useDeferredValue, memo } from 'react';

interface DataPoint {
  id: string;
  category: string;
  value: number;
  timestamp: number;
}

function Dashboard({ rawData }: { rawData: DataPoint[] }) {
  const [filters, setFilters] = useState({
    category: '',
    minValue: 0,
  });

  // ✅ 延迟过滤后的数据,控制按钮可以立即响应
  const deferredFilteredData = useDeferredValue(
    useMemo(() => {
      let result = rawData;

      if (filters.category) {
        result = result.filter(d => d.category === filters.category);
      }

      if (filters.minValue > 0) {
        result = result.filter(d => d.value >= filters.minValue);
      }

      return result;
    }, [rawData, filters])
  );

  const handleCategoryChange = (category: string) => {
    setFilters(prev => ({ ...prev, category }));
  };

  const handleMinValueChange = (minValue: number) => {
    setFilters(prev => ({ ...prev, minValue }));
  };

  return (
    <div className="p-4">
      {/* 控制面板 - 立即响应 */}
      <div className="mb-6 p-4 bg-gray-50 rounded">
        <h3 className="font-semibold mb-3">过滤器</h3>

        <div className="space-y-3">
          <div>
            <label className="block text-sm mb-1">类别</label>
            <select
              value={filters.category}
              onChange={(e) => handleCategoryChange(e.target.value)}
              className="w-full border p-2 rounded"
            >
              <option value="">全部</option>
              <option value="A">类别 A</option>
              <option value="B">类别 B</option>
              <option value="C">类别 C</option>
            </select>
          </div>

          <div>
            <label className="block text-sm mb-1">最小值</label>
            <input
              type="number"
              value={filters.minValue}
              onChange={(e) => handleMinValueChange(Number(e.target.value))}
              className="w-full border p-2 rounded"
              placeholder="输入最小值..."
            />
          </div>
        </div>
      </div>

      {/* 图表 - 延迟渲染 */}
      <ExpensiveChart data={deferredFilteredData} />
    </div>
  );
}

// ✅ 使用 memo 优化图表组件,只在 data 变化时重新渲染
const ExpensiveChart = memo(function ExpensiveChart({ data }: { data: DataPoint[] }) {
  // 假设这是一个很重的图表渲染组件
  return (
    <div className="p-4 bg-white border rounded">
      <h3 className="font-semibold mb-3">数据可视化</h3>
      <div className="h-64 flex items-center justify-center text-gray-400">
        图表区域 ({data.length} 条数据)
      </div>
    </div>
  );
});

效果: 用户调整过滤器时,控制面板立即响应(下拉框关闭、输入框显示值), 图表在后台更新。即使数据量大、计算复杂,界面始终保持可交互。

示例 3: 从 Props 派生的复杂 UI(典型场景)

当父组件频繁更新时,子组件可能不需要每次都立即更新。使用延迟值优化渲染。

TypeScript
import { useDeferredValue, memo, useState, useEffect } from 'react';

interface ProductListProps {
  products: Product[];
  sortBy: 'name' | 'price' | 'rating';
}

// 父组件 - 频繁更新
function ProductPage() {
  const [products, setProducts] = useState<Product[]>([]);
  const [sortBy, setSortBy] = useState<'name' | 'price' | 'rating'>('name');
  const [cart, setCart] = useState<CartItem[]>([]);

  // 模拟数据频繁更新(如实时数据、定时刷新)
  useEffect(() => {
    const interval = setInterval(() => {
      // 模拟数据更新
      setProducts(prev => updateProducts(prev));
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  const addToCart = (product: Product) => {
    setCart(prev => [...prev, { product, quantity: 1 }]);
  };

  return (
    <div className="p-4">
      {/* 购物车 - 立即响应用户操作 */}
      <CartSummary cart={cart} onAddToCart={addToCart} />

      {/* 排序控制 - 立即响应用户操作 */}
      <SortControl sortBy={sortBy} onSortChange={setSortBy} />

      {/* 产品列表 - 延迟渲染,因为数据频繁更新 */}
      <ProductList products={products} sortBy={sortBy} />
    </div>
  );
}

// ✅ 子组件使用延迟值
function ProductList({ products, sortBy }: ProductListProps) {
  // ✅ 延迟 products 和 sortBy,列表渲染不会阻塞购物车和排序控制
  const deferredProducts = useDeferredValue(products);
  const deferredSortBy = useDeferredValue(sortBy);

  // ✅ 基于延迟值计算排序后的列表
  const sortedProducts = useMemo(() => {
    return [...deferredProducts].sort((a, b) => {
      switch (deferredSortBy) {
        case 'name':
          return a.name.localeCompare(b.name);
        case 'price':
          return a.price - b.price;
        case 'rating':
          return b.rating - a.rating;
        default:
          return 0;
      }
    });
  }, [deferredProducts, deferredSortBy]);

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
      {sortedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// ✅ 使用 memo 优化产品卡片
const ProductCard = memo(function ProductCard({ product }: { product: Product }) {
  return (
    <div className="border rounded p-4">
      <h3 className="font-semibold">{product.name}</h3>
      <p className="text-sm text-gray-600">¥{product.price}</p>
      <p className="text-sm text-yellow-500">⭐ {product.rating}</p>
    </div>
  );
});

效果: 即使 products 每秒都在更新,购物车和排序控制也能保持响应。 产品列表会在浏览器空闲时更新,不会阻塞用户操作。

示例 4: 大型列表性能优化(高级场景)

结合虚拟滚动和延迟值,优化超大型列表的渲染性能。

TypeScript
import { useDeferredValue, useMemo, useState } from 'react';

// 假设使用 react-window 或 react-virtual
import { FixedSizeList } from 'react-window';

function LargeList({ allItems }: { allItems: Item[] }) {
  const [filter, setFilter] = useState('');

  // ✅ 延迟过滤条件,列表渲染不会阻塞输入
  const deferredFilter = useDeferredValue(filter);

  // ✅ 基于延迟值过滤
  const filteredItems = useMemo(() => {
    if (!deferredFilter) return allItems;

    return allItems.filter(item =>
      item.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [allItems, deferredFilter]);

  // ✅ 渲染每一行
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    const item = filteredItems[index];

    return (
      <div style={style} className="p-2 border-b">
        {item.name} - {item.description}
      </div>
    );
  };

  return (
    <div>
      {/* 输入框 - 立即响应 */}
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="过滤项目..."
        className="w-full border p-2 rounded mb-4"
      />

      {/* 虚拟列表 - 延迟更新 */}
      <FixedSizeList
        height={600}
        itemCount={filteredItems.length}
        itemSize={50}
        width="100%"
      >
        {Row}
      </FixedSizeList>

      <p className="mt-2 text-sm text-gray-500">
        显示 {filteredItems.length} / {allItems.length} 条
      </p>
    </div>
  );
}

// 使用示例:假设有 100,000 条数据
const hugeList = Array.from({ length: 100000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  description: `Description for item ${i}`,
}));

// <LargeList allItems={hugeList} />

效果: 即使有 100,000 条数据,用户输入时也不会卡顿。 虚拟滚动只渲染可见部分,延迟值确保输入框始终保持响应。

避坑指南

❌ 陷阱 1: 将文本输入框的值用 useDeferredValue 包裹

问题: 文本输入需要立即更新,否则会感觉"延迟"或"卡顿"。 将输入框的值本身用 useDeferredValue 包裹会导致打字不跟手。

TypeScript
// ❌ 错误: 输入框的值本身不应该用 useDeferredValue
function SearchInput() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query); // ❌ 会导致打字延迟!

  return (
    <input
      value={deferredQuery} // ❌ 输入会延迟显示
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

// ✅ 正确: 输入框用原值,搜索结果用延迟值
function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query); // ✅ 只用于搜索结果

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [items, deferredQuery]);

  return (
    <div>
      {/* ✅ 输入框用原值,立即响应 */}
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      {/* ✅ 搜索结果用延迟值 */}
      <ResultsList items={filteredItems} />
    </div>
  );
}

心智模型纠正: 输入框本身需要立即响应用户输入, 只有基于输入值派生的 UI(如搜索结果、过滤列表)才需要延迟更新。

❌ 陷阱 2: 误认为 useDeferredValue 是防抖/节流

问题: useDeferredValue 不是防抖/节流,它不会减少更新次数或延迟计算。 它只是调整渲染的优先级,让紧急更新先处理。

TypeScript
// ❌ 错误理解: 认为 useDeferredValue 会"减少"计算
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
  const deferredFilter = useDeferredValue(filter);

  // ❌ 错误理解: 以为这样会"减少"过滤次数
  // 实际上: 每次渲染都会执行过滤,只是优先级较低
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [items, deferredFilter]);

  return <List items={filteredItems} />;
}

// ✅ 正确理解: useDeferredValue 调整渲染优先级
// 如果真的要减少计算,应该用防抖
function ExpensiveListWithDebounce({ items, filter }: { items: Item[]; filter: string }) {
  // ✅ 使用防抖减少计算频率
  const debouncedFilter = useDebounce(filter, 300);

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(debouncedFilter.toLowerCase())
    );
  }, [items, debouncedFilter]);

  return <List items={filteredItems} />;
}

// ✅ 或者结合使用: 防抖 + 延迟值
function OptimizedList({ items, filter }: { items: Item[]; filter: string }) {
  const debouncedFilter = useDebounce(filter, 300);
  const deferredFilter = useDeferredValue(debouncedFilter);

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [items, deferredFilter]);

  return <List items={filteredItems} />;
}

心智模型纠正: useDeferredValue 是"优先级管理工具", 不是"性能优化工具"。它让界面保持响应,但不减少计算量。 要减少计算,需要用防抖/节流、useMemo 等。

❌ 陷阱 3: 延迟每次渲染都创建的新对象

问题: 如果传入 useDeferredValue 的值每次渲染都是新对象/数组, 即使内容相同,延迟值也会每次更新,失去优化效果。

TypeScript
// ❌ 错误: 每次渲染都创建新对象
function Component({ items }: { items: Item[] }) {
  // ❌ 每次渲染都创建新数组,延迟值会频繁更新
  const filtered = useDeferredValue(
    items.filter(item => item.active) // 每次都是新数组
  );

  return <ExpensiveList items={filtered} />;
}

// ✅ 正确: 先用 useMemo 稳定引用,再用 useDeferredValue
function Component({ items }: { items: Item[] }) {
  // ✅ 先用 useMemo 缓存计算结果
  const filtered = useMemo(() => {
    return items.filter(item => item.active);
  }, [items]);

  // ✅ 再用 useDeferredValue 延迟渲染
  const deferredFiltered = useDeferredValue(filtered);

  return <ExpensiveList items={deferredFiltered} />;
}

心智模型纠正: useDeferredValue 使用 Object.is 比较值。 如果每次都是新引用,即使内容相同,也会被认为是"变化"了。 应该先用 useMemo 稳定引用,再用 useDeferredValue 延迟渲染。

❌ 陷阱 4: 过度使用 useDeferredValue

问题: 不是所有值都需要延迟。简单的、快速的渲染用延迟值反而增加复杂度。

TypeScript
// ❌ 错误: 简单值不需要延迟
function SimpleComponent() {
  const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count); // ❌ 没必要!

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      <p>计数: {deferredCount}</p>
    </div>
  );
}

// ✅ 正确: 简单值直接使用
function SimpleComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      <p>计数: {count}</p>
    </div>
  );
}

// ✅ 正确: 只有导致重型渲染的值才延迟
function ComplexComponent({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');

  // ✅ 延迟会导致重型列表渲染的值
  const deferredFilter = useDeferredValue(filter);

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [items, deferredFilter]);

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <HeavyList items={filteredItems} />
    </div>
  );
}

心智模型纠正: useDeferredValue 用于"可能导致重型渲染的值", 简单的、快速的值直接使用即可。

最佳实践

✅ 推荐模式

1. 结合 useMemo 使用

先用 useMemo 稳定引用和缓存计算,再用 useDeferredValue 延迟渲染。

TypeScript
function SearchResults({ items, filter }: { items: Item[]; filter: string }) {
  // ✅ 第一步: 用 useMemo 缓存计算结果
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // ✅ 第二步: 用 useDeferredValue 延迟渲染
  const deferredFilteredItems = useDeferredValue(filteredItems);

  // ✅ 第三步: 基于延迟值渲染
  return <ResultsList items={deferredFilteredItems} />;
}

2. 配合 React.memo 使用

使用 React.memo 优化子组件,只在延迟值变化时重新渲染。

TypeScript
// ✅ 使用 memo 优化子组件
const ExpensiveChart = memo(function ExpensiveChart({ data }: { data: DataPoint[] }) {
  // 重型图表渲染逻辑
  return <Chart data={data} />;
});

function Dashboard({ rawData }: { rawData: DataPoint[] }) {
  const [filters, setFilters] = useState({});

  // ✅ 延迟过滤后的数据
  const deferredData = useDeferredValue(
    useMemo(() => applyFilters(rawData, filters), [rawData, filters])
  );

  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      {/* ✅ 只在 deferredData 变化时重新渲染 */}
      <ExpensiveChart data={deferredData} />
    </div>
  );
}

3. 与防抖结合使用

对于高频更新的值,先用防抖减少计算频率,再用延迟值保持响应性。

TypeScript
function OptimizedSearch({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');

  // ✅ 先防抖,减少计算频率
  const debouncedQuery = useDebounce(query, 300);

  // ✅ 再延迟,保持响应性
  const deferredQuery = useDeferredValue(debouncedQuery);

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [items, deferredQuery]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ResultsList items={filteredItems} />
    </div>
  );
}

4. 延迟 Props 派生的值

当父组件频繁更新时,子组件可以延迟基于 props 派生的值。

TypeScript
// ✅ 子组件延迟处理 props
function ChildComponent({ data }: { data: Data[] }) {
  // ✅ 延迟从 props 派生的值
  const deferredData = useDeferredValue(data);

  const sortedData = useMemo(() => {
    return [...deferredData].sort((a, b) => a.value - b.value);
  }, [deferredData]);

  return <DataGrid data={sortedData} />;
}

// 父组件频繁更新
function ParentComponent() {
  const [data, setData] = useState<Data[]>([]);

  // 模拟数据频繁更新
  useEffect(() => {
    const interval = setInterval(() => {
      setData(refreshData);
    }, 100);

    return () => clearInterval(interval);
  }, []);

  return <ChildComponent data={data} />;
}

⚠️ 使用建议

  • 先确保正确,再考虑性能: 不要一开始就用 useDeferredValue, 先实现功能,如果发现卡顿再优化
  • 测量性能: 使用 React DevTools Profiler 测量渲染时间, 确认瓶颈确实是由渲染导致的
  • 渐进式优化: 一次只优化一个部分, 观察 effect,避免过度优化
  • 设备测试: 在不同性能的设备上测试, 确保在低端设备上也有改善

📊 useDeferredValue vs useTransition vs 防抖

三种工具的适用场景不同,可以配合使用:

工具主要用途控制粒度典型场景
useTransition标记状态更新为低优先级控制一组 setState 调用用户触发的更新(搜索、Tab 切换)
useDeferredValue延迟值的渲染控制单个值的使用时机从 props/state 派生的复杂 UI
防抖/节流减少计算/更新频率控制值的变化频率高频事件(输入、滚动、resize)

推荐: 如果要控制"何时 setState",使用 useTransition。 如果要延迟"使用某个值的 UI",使用 useDeferredValue。 如果要减少"计算频率",使用防抖/节流。 三者可以结合使用达到最佳性能。

延伸阅读

这篇文章有帮助吗?
这篇文章有帮助吗?