React 中 useDeferredValue
和 startTransition
,两者都用于优化性能,但适用场景和实现方式不同。
核心概念解析
1. useDeferredValue:延迟值更新
-
作用 :告诉 React 延迟更新某个值,直到所有高优先级渲染任务完成。
-
适用场景:当某个值的更新会触发计算密集型渲染(如复杂列表、图表),但需要优先保证其他高优先级 UI(如输入框)的响应。
-
机制:
- 保持旧值显示,后台计算新值。
- 计算完成后,用新值更新 UI,期间允许用户继续交互。
-
示例:
假设有一个输入框和一个渲染大量数据的列表,每次输入都会触发列表过滤。直接用
query
过滤时,输入会卡顿;用useDeferredValue
后,输入体验会明显提升。
jsx
import React, { useState, useDeferredValue, useMemo } from 'react';
// 假设有一个很大的用户列表
const largeList = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`,
}));
function ExpensiveList({ filter }) {
// 这里用 useMemo 模拟一次很慢的过滤操作
const filteredList = useMemo(() => {
// 模拟耗时操作
const start = performance.now();
while (performance.now() - start < 10) {} // 人为阻塞10ms
return largeList.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [filter]);
return (
<ul>
{filteredList.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default function App() {
const [query, setQuery] = useState('');
// 获取延迟后的 query
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索用户"
/>
{/* 这里用延迟值,保证输入框流畅 */}
<ExpensiveList filter={deferredQuery} />
{/* 你也可以对比直接用 query,体验会卡顿 */}
{/* <ExpensiveList filter={query} /> */}
</div>
);
}
说明
query
:输入框内容,实时更新,优先保证输入体验。deferredQuery
:用useDeferredValue
得到的延迟值,只有在主线程空闲时才会更新,传递给耗时组件。ExpensiveList
:模拟一个渲染和过滤都很慢的组件。- 这样写的好处是:输入框始终流畅,列表渲染不会阻塞用户输入,即使数据量很大也能保证良好体验。
2. startTransition:显式标记过渡更新
-
作用 :显式告知 React 某个状态更新可能触发高开销渲染,应作为低优先级任务处理。
-
适用场景:开发者能直接控制状态更新逻辑时(如搜索框输入后过滤列表)。
-
机制:
- 包裹状态更新代码,标记为"过渡任务"。
- React 优先处理高优先级事件(如用户输入),过渡任务可被中断或延迟。
-
示例:
假设有一个用户列表,根据输入内容实时过滤,过滤操作假设很耗时。
jsx
import React, { useState, useTransition } from 'react';
// 假设这是一个渲染大量数据的组件
function ExpensiveList({ users }) {
// 这里可以模拟耗时渲染,比如用 Array.map 渲染很多项
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default function App({ allUsers }) {
const [query, setQuery] = useState('');
const [filteredUsers, setFilteredUsers] = useState(allUsers);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 立即更新输入框内容(高优先级)
// 过滤操作作为"过渡"处理(低优先级,可能很慢)
startTransition(() => {
const filtered = allUsers.filter(user =>
user.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredUsers(filtered);
});
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="搜索用户" />
{isPending && <div>加载中...</div>}
<ExpensiveList users={filteredUsers} />
</div>
);
}
说明
query
:输入框的内容,属于高优先级,用户每输入一次立即更新。filteredUsers
:过滤后的用户列表,属于低优先级,用startTransition
包裹,React 会在主线程空闲时再去处理它。isPending
:表示过滤操作是否还在进行,可以用来显示加载提示。ExpensiveList
:渲染过滤后的用户列表,假设渲染很多数据,比较耗时。
对比
特性 | useDeferredValue |
startTransition |
---|---|---|
控制对象 | 值(如 state 或 prop) | 状态更新操作(如 setState 调用) |
适用场景 | 无法直接控制状态更新(如值来自父组件) | 能直接控制状态更新逻辑(如事件处理函数) |
优先级控制 | 自动延迟低优先级值的更新 | 显式标记特定更新为低优先级 |
实现方式 | 通过缓存旧值,后台计算新值 | 包裹更新代码,调度为可中断任务 |
与 UI 的关系 | 延迟更新 UI 中的某个部分 | 延迟更新由状态变化触发的 UI |
开发者控制权 | 被动:由 React 自动调度 | 主动:开发者显式标记过渡任务 |
底层原理
useDeferredValue
-
实现 :内部结合了
useState
、useEffect
,并利用 React 的并发调度(Transition)机制。 -
流程:
- 组件每次渲染时,
useDeferredValue
先返回上一次的"延迟值"。 - 当依赖的原始值变化时,
useEffect
触发,在一个过渡任务(transition)中异步更新延迟值。 - 如果在延迟值尚未更新期间原始值再次变化,之前的过渡任务会被中断,重新发起新的过渡任务,确保只渲染最新的值。
- 这样可以保证高优先级的交互(如输入)不会被低优先级的重渲染阻塞,提升用户体验。
- 组件每次渲染时,
startTransition
-
实现 :
startTransition
会临时切换 React 内部的优先级配置,仅将包裹在回调函数中的状态更新标记为"过渡任务"(低优先级),并发模式下这些任务可以被中断或延后执行。 -
流程:
- 调用
startTransition
时,React 内部会设置一个"过渡任务"标志,仅对本次回调函数内的状态更新生效。 - 执行回调函数,回调中的所有状态更新(如
setState
)会被标记为低优先级。 - 回调执行完毕后,React 会恢复原有的优先级配置,后续的状态更新不再受影响。
- 这样,只有你明确包裹在
startTransition
回调里的状态更新是低优先级,其他更新(如用户输入、点击等)依然是高优先级,可以优先响应用户操作。
- 调用
总结
-
useDeferredValue
:用于被动延迟某个值的更新,优化显示复杂 UI 时的响应性。 -
startTransition
:用于主动标记高开销更新,明确控制优先级。 -
共同目标:避免主线程阻塞,优先处理用户交互,提升应用流畅度。
-
选择建议:
- 若状态来自外部(如 props),用
useDeferredValue
。 - 若直接控制状态更新,用
startTransition
。
- 若状态来自外部(如 props),用