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 渲染。
技术规格
类型签名
function useDeferredValue<T>(value: T): T参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
value | T | 要延迟的值,可以是任何类型(字符串、数字、对象、数组等) |
返回值
返回一个与输入值类型相同的值,但更新时机被"延迟":
- 首次渲染: 返回传入的 value
- 后续渲染: 如果 value 变化,React 可能在当前渲染中立即返回旧值, 待紧急更新处理完后再重新渲染使用新值
- 值未变化: 如果 value 与上次相同(Object.is 相等), 返回相同的引用,不触发依赖该值的组件重新渲染
运行机制
useDeferredValue 基于 React 18 的并发渲染特性,底层机制如下:
- 优先级标记: 使用延迟值的组件被标记为低优先级更新
- 时间切片: React 将渲染工作分解成小单元, 在每个时间切片后检查是否有更高优先级的更新
- 可中断渲染: 如果在延迟渲染过程中有新的紧急更新(如用户输入), React 会放弃当前的延迟渲染,处理紧急更新后再重新开始
- 值比较: 使用
Object.is比较新旧值, 如果值未变化,复用上次的值,跳过重新渲染
注意: useDeferredValue 需要 React 18+ 和支持并发渲染的渲染器 (如 ReactDOM 18+ 的 createRoot)。使用旧版 ReactDOM.render() 不会启用并发特性。
实战演练
示例 1: 搜索输入框(基础用法)
最常见的场景:用户快速输入时,输入框立即响应,搜索结果延迟更新。
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: 数据可视化仪表板(生产级)
数据频繁更新时,复杂图表的重绘会阻塞界面。使用延迟值保持控制按钮响应。
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(典型场景)
当父组件频繁更新时,子组件可能不需要每次都立即更新。使用延迟值优化渲染。
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: 大型列表性能优化(高级场景)
结合虚拟滚动和延迟值,优化超大型列表的渲染性能。
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 包裹会导致打字不跟手。
// ❌ 错误: 输入框的值本身不应该用 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 不是防抖/节流,它不会减少更新次数或延迟计算。 它只是调整渲染的优先级,让紧急更新先处理。
// ❌ 错误理解: 认为 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 的值每次渲染都是新对象/数组, 即使内容相同,延迟值也会每次更新,失去优化效果。
// ❌ 错误: 每次渲染都创建新对象
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
问题: 不是所有值都需要延迟。简单的、快速的渲染用延迟值反而增加复杂度。
// ❌ 错误: 简单值不需要延迟
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 延迟渲染。
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 优化子组件,只在延迟值变化时重新渲染。
// ✅ 使用 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. 与防抖结合使用
对于高频更新的值,先用防抖减少计算频率,再用延迟值保持响应性。
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 派生的值。
// ✅ 子组件延迟处理 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。 如果要减少"计算频率",使用防抖/节流。 三者可以结合使用达到最佳性能。