useDeferredValue 与响应预算
问题:只有一个查询 state,怎样让慢列表晚一点追赶
有时你只拥有 query,无法或不想维护第二份 state。输入框应读最新 query,昂贵子树可以读 deferredQuery。React 会先用旧延迟值完成紧急渲染,再在后台尝试包含新值的渲染。
完整实验:陈旧结果仍可见
import { memo, useDeferredValue, useState } from 'react'
const records = Array.from({ length: 300 }, (_, id) => ({
id,
title: `Record ${String(id).padStart(4, '0')}`,
}))
function SlowRow({ record, query }) {
const start = performance.now()
while (performance.now() - start < 1) {} // 仅用于实验
return record.title.toLowerCase().includes(query) ? <li>{record.title}</li> : null
}
const SlowResults = memo(function SlowResults({ query }) {
return <section>
<p>显示查询:{query || '空'}</p>
<ul>{records.map(record =>
<SlowRow key={record.id} record={record} query={query.toLowerCase()} />,
)}</ul>
</section>
})
export default function App() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<main>
<h1>记录搜索</h1>
<input aria-label="查询" value={query} onChange={(event) => setQuery(event.target.value)} />
<div
aria-busy={isStale}
style={{ opacity: isStale ? 0.55 : 1, transition: 'opacity 160ms' }}
>
{isStale && <p>结果正在更新,当前仍显示旧查询。</p>}
<SlowResults query={deferredQuery} />
</div>
</main>
)
}第一次渲染中两个值相同。输入变化后,React 先尝试用新 query 和旧 deferredQuery 渲染,因此输入立即回显、结果带陈旧提示;随后在后台尝试让 deferredQuery 追上。若又输入字符,后台工作可被丢弃并从新值重来。
memo 很重要:父组件紧急渲染时,props 仍是旧 deferredQuery,慢子树可跳过;没有 memo,子组件仍会随父组件执行,效果可能不明显。React Compiler 可自动处理部分记忆化,但课程示例显式写出边界。
慢工作分布在各个 SlowRow,React 可以在组件工作之间响应更紧急更新。一次 filter 里进行几百毫秒计算仍是不可中断的同步调用,延迟值不会把它拆开。这里也不能把 <SlowRow /> 改成直接调用函数;组件边界与 props 不变时的跳过条件,分别承担“可以让出工作”和“紧急路径不用重做”的责任。
延迟值由 Object.is 比较。若每次渲染都构造 { query } 再交给 useDeferredValue,即使查询没变,对象身份也变了,会安排多余后台工作。优先延迟 query 这样的原始值;确需对象时,先按第三章的规则稳定其身份。已经处在 Transition 中的更新本来就是非紧急的,此时 useDeferredValue 可直接返回新值,不需要再套一轮延迟。
它不是定时器
useDeferredValue 没有固定延迟,React 会根据其他工作调度。它不同于防抖:每个新值仍可能进入后台渲染,也不会减少网络请求。若需求是“停止输入 300ms 后才请求”,应实现明确防抖或使用数据层策略;若需求是“输入优先、旧结果暂时可读”,延迟值更贴切。
关键变体与预算
- 延迟值可与 Suspense 配合:新结果 suspend 时继续显示旧结果,并用
isStale降低透明度。 - 背景渲染的 Effect 只有提交后才运行;被放弃的树不会运行 Effect。
- 先用 React Profiler、浏览器 Performance 或输入延迟观察建立基线。小列表没卡顿时,引入 memo/延迟只增加复杂度。
- 延迟不能修复 O(n²) 算法或过大的 DOM;先减少工作量,再安排剩余工作。
练习:契约任务
把数据改为书名数组并实现:
- 输入立即显示
typed: <query>,结果显示shown: <deferredQuery>。 - 匹配忽略首尾空格和大小写;空查询显示全部。
- 两值不同时设置
aria-busy="true"并显示陈旧提示,相同后消失。 - 把匹配提取为纯函数,并用断言覆盖空查询、大小写和无结果;不得把延迟值当作减少请求次数的方案。
完成后对照答案 05-03。