news 2026/9/19 4:19:44

React并发渲染:useDeferredValue与useTransition的本质区别与场景选择

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React并发渲染:useDeferredValue与useTransition的本质区别与场景选择

React 并发渲染的面试题里,useDeferredValueuseTransition这对兄弟出现的频率越来越高。很多朋友学完这两个 API 之后的第一反应是:这俩不都是让界面别那么卡的吗?到底有啥区别?我在实际项目里试过之后发现,它们背后的设计思路完全不同,用错的代价也很大。这篇文章我就结合自己的踩坑经历,把这两个 API 的底层逻辑、使用场景、以及它们之间真正的区别讲透。

先说一句结论:useTransition是让你把“一个状态更新”标记为低优先级,useDeferredValue是让你把“一个值”在渲染时延迟派生。一个是主动让更新让路,一个是被动让值慢半拍。看起来差不多,但底层机制和适用场景差得远。

1. 这俩 API 到底在解决什么问题

1.1 React 18 之前的“卡顿”是怎么来的

在 React 18 之前,所有状态更新都是同步渲染的。什么意思呢?就是一旦你调用了setState,React 会立刻开始协调(reconcile)虚拟 DOM,然后提交到真实 DOM,这个过程中主线程是被占用的。如果组件树很庞大、渲染很耗时,用户在这段时间里点按钮、敲键盘、滚动页面,全部会被卡住,因为浏览器的主线程没空处理这些交互事件。

我举个直观的例子:一个搜索框,input 每敲一个字符就触发一次过滤,过滤一个两万条数据的列表。每次敲键都要同步跑完整个列表的过滤和渲染,低端手机上妥妥的卡成幻灯片。React 官方的说法叫“渲染阻塞交互”,本质就是同步渲染占用了主线程太久。

1.2 并发渲染和 Fiber 的作用

React 18 引入并发特性(Concurrent Features)之后,局面才发生了转变。核心变化是渲染不再是“一口气到底”的同步任务,而是分片执行的。这背后就是 Fiber 架构在起作用——每个组件对应的 Fiber 节点可以被拆成一个个小单元,渲染过程可以在这些单元之间暂停、让出主线程,等浏览器处理完紧急的输入事件之后再回来继续渲染。

打个比方:以前同步渲染是一条单行道,所有车(渲染任务)必须依次通过,后面的车(用户输入)只能等着。并发渲染变成了一条多车道,紧急的车(用户输入、点击)可以插队先走,不急的车(列表过滤、大图表更新)稍微绕一下路也没关系。

这个“让路”和“插队”的机制,就是useTransitionuseDeferredValue共同的底层基础。没有并发渲染,这两个 API 就是空中楼阁。

1.3 紧急更新 vs 过渡更新

这两个 API 的核心思想可以归结为一句话:区分紧急更新和过渡更新。

什么是紧急更新?输入框里打字、点击按钮、切换 Tab、拖拽滑块——这些用户能直接感知到的操作,需要立刻反馈,这是紧急更新。什么是过渡更新?根据输入内容过滤列表、根据选择生成图表、切换页面后加载和渲染大组件——这些内容需要的结果,但用户能接受稍微晚几十毫秒甚至一两百毫秒出现,这是过渡更新。

问题来了:在一个 setState 触发的复杂渲染过程中,React 怎么知道哪些部分是紧急的,哪些部分是过渡的?单纯靠 React 自动判断是不现实的,所以 React 把选择权交给你了。useTransitionuseDeferredValue就是你用来告诉 React“这个不急,可以先放一放”的两把工具。

2. useTransition:主动让更新让路

2.1 基本用法:把状态更新放进 startTransition

useTransition的用法非常直观。它返回两个东西:isPendingstartTransitionisPending是一个布尔值,告诉我们当前是否有过渡更新正在进行;startTransition是一个函数,你把需要降级的那个状态更新放进它的回调里就行。

来看一个最常见的搜索列表场景:

import { useState, useTransition } from 'react'; function SearchPage() { const [query, setQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; // 紧急更新:输入框立即显示用户敲的内容 setQuery(value); // 过渡更新:大列表的过滤结果可以慢一点 startTransition(() => { setSearchResults(filterLargeList(value)); }); }; return ( <div> <input value={query} onChange={handleChange} /> {isPending ? <div>加载中...</div> : <ResultList list={searchResults} />} </div> ); }

注意这段代码里的一个关键细节:输入框本身的更新setQuery(value)没有被包进startTransition,只有setSearchResults被包进去了。这就是useTransition的典型使用模式——把紧急更新和过渡更新拆开,保输入框的即时响应,让结果列表慢慢来。

2.2 原理:startTransition 标记的是更新本身

那么startTransition内部到底做了什么?从源码层面讲,它做了一件很简单的事:把回调里的更新标记为 Transition 优先级(在 React 的调度器里对应一个比普通更新更低的优先级)。被标记为 Transition 的更新在调度时,会被排在紧急更新之后执行,而且可以被更高优先级的更新打断。

这就是我前面说的“主动让路”的含义——你通过startTransition这个 API,明确告诉 React:这个更新不着急,你可以先处理别的紧急事情,有空了再来完成它。如果用户在这期间又输入了新的内容,之前的过渡更新会被直接放弃,React 只会处理最新的一次。

这里有一个重要的认知:startTransition作用于“更新”这个动作本身。它包裹的是setState的调用,而不是某个值。在 concurrent 模式下,低优先级的更新在其渲染过程中会读到最新的 state,所以用户不会看到陈旧的内容——你输入“abc”的时候,如果过滤计算正在处理“ab”阶段的结果,React 不会渲染出“ab”的旧结果,这个低优先级更新会被后续的高优先级更新打断和覆盖,最终只会渲染出基于“abc”的最新结果。这也为什么过渡更新不会导致界面显示过时数据。

2.3 注意别把输入框更新也包进去

很多新手在刚用useTransition的时候,很容易把所有 setState 一股脑包进去,比如这样:

// ❌ 错误示例 const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; startTransition(() => { setQuery(value); // 连输入框的值也变“慢”了 setSearchResults(filterLargeList(value)); }); };

这个写法会导致什么结果?输入框的响应也变迟钝了。因为setQuery也被标记成了低优先级更新,用户在键盘上敲下一个字符,输入框里可能要等几十毫秒才会显示出来,这种体验比卡顿还糟糕——用户会以为键盘坏了。

记住一个原则:用户直接操作的那个东西,永远是紧急更新,不能包进startTransitionstartTransition只用来包住那些“跟随用户操作而更新、但不需要立刻反映在界面上”的状态。

3. useDeferredValue:被动让值慢半拍

3.1 基本用法:把值延迟派生

useDeferredValue的用法和useTransition完全不一样。它不需要你包裹任何 setState,它接收一个值,返回一个延迟版本的值:

import { useState, useDeferredValue, useMemo } from 'react'; function SearchPage() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const searchResults = useMemo(() => { return filterLargeList(deferredQuery); }, [deferredQuery]); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ResultList list={searchResults} /> </div> ); }

这段代码和前面useTransition的例子达到了类似的效果:输入框立刻显示用户敲入的query,而下面的列表渲染的是deferredQuery这个延迟版本的值。用户敲击键盘时,input 保持流畅,列表在后台慢慢更新。

这里的注意点是:useDeferredValue(query)不是一个同步拷贝。在紧急更新触发重新渲染时,React 先用旧值渲染一次(为了快速响应用户操作),然后会在后台用新值再渲染一次。也就是说,你可能看到列表中的结果短暂地比输入的内容“慢一版”,但这是刻意为之的。

3.2 原理:延迟的是渲染读取到的值

useDeferredValue的实现机制和useTransition不同。它并不是把某个 setState 标记为低优先级,而是利用了 React 的 concurrent 渲染能力——在 render 阶段,React 读到的 deferred value 是旧值,于是这次渲染会以低优先级进行。当 React 有空闲时,再用新值触发一次后台渲染。这就是“被动延迟”的含义:你没有主动指定哪个更新是低优先级的,你只是在渲染时读取了一个被延迟后传递给子组件的值,React 内部为这个值相关的渲染安排了较低的优先级。

我自己在使用时的感觉是:useDeferredValue更像是一个“渲染层的防抖”,但它是基于并发渲染的,不是基于定时器的。它不会像防抖那样固定延迟几百毫秒,而是优先把浏览器空闲时间让给用户输入,在空闲时段来执行低优先级渲染。

还有一个关键点:useDeferredValue是原始值的一个快照。当原始值在一帧内多次变化时,React 只会在最终状态确定后再去更新 deferred value 对应的渲染。这样可以显著减少因为连续输入而触发的多余渲染次数。

3.3 别把 useDeferredValue 当 useMemo 用

useDeferredValueuseMemo用途不同,但有些人会混在一起。举一个常见的错误用法:

// ❌ 这没什么意义 const [count, setCount] = useState(0); const deferredCount = useDeferredValue(count);

count是一个简单的数字,把它 defer 之后没有任何意义,因为它没有什么昂贵的渲染计算需要让路。你并不会因为延迟一个数字就变快,反而会在不该延迟的地方引入额外的复杂度。

再举一个容易踩坑的组合:

// ❌ useMemo 的依赖写反了 const results = useMemo(() => { return expensiveFilter(query, list); }, [query]); // 应该用 deferredQuery

如果你在useDeferredValue后面跟着一个useMemo,但useMemo的依赖数组里写的是原始值而不是延迟值,这个useMemo每次都会立刻重新计算,延迟效果就完全失效了。正确写法永远是把deferredQuery作为useMemo的依赖。

4. 核心区别:一张表看清 useDeferredValue 和 useTransition

4.1 本质差异:更新 vs 值

前面铺垫了那么多,现在可以正面回答题目问题了。useTransitionuseDeferredValue的本质区别在于操作对象:

对比维度useTransitionuseDeferredValue
操作对象状态更新(setState)值本身(state/props)
核心 APIstartTransition(fn) 包裹状态更新逻辑useDeferredValue(value) 接收值,返回延迟值
使用方式你可以控制哪些 setState 是低优先级的React 根据渲染流程自动延迟该值相关的更新
判断更新状态isPending 可以标记过渡任务是否进行中没有等价的 isPending,但可以通过渲染值是否变化推断
心智模型主动让更新让路被动让值慢半拍
适用场景多个 setState 联动时,部分紧急、部分过渡单个值驱动开销很大的渲染时
灵活性高,可以精确控制每一处更新的优先级低,只能针对某个值做延迟

把这张表展开来说,useTransition最核心的一句话是“主动把更新标记为低优先级”。这种主动标记可以在一个事件回调里做出精确的判断——例如输入框的值紧急更新,列表结果过渡更新。你甚至可以一个回调里有多个 setState,有的紧急、有的过渡。

反过来,useDeferredValue是“值级别的延迟”。它没有startTransition这种 API,而是把状态值先延迟一层,任何依赖这个延迟值的渲染都会变成过渡更新。这其实更适合你只关心某个值、不想去区分多个 setState 的场景。

4.2 在同一个场景中的代码对比

我再扩展一下这段代码对比,让可读性更直观。假设你有一个表格,数据量很大,需要根据输入框筛选:

// useTransition 写法 function TablePage() { const [keyword, setKeyword] = useState(''); const [tableData, setTableData] = useState(fullData); const [isPending, startTransition] = useTransition(); const handleKeywordChange = (e) => { const value = e.target.value; setKeyword(value); // 紧急 startTransition(() => { setTableData(fullData.filter(row => row.name.includes(value))); // 过渡 }); }; }
// useDeferredValue 写法 function TablePage() { const [keyword, setKeyword] = useState(''); const deferredKeyword = useDeferredValue(keyword); const tableData = useMemo(() => { return fullData.filter(row => row.name.includes(deferredKeyword)); }, [deferredKeyword]); }

注意两种写法的核心差异:

  • 第一种写法,setTableData本身是一个状态,它存储了过滤后的数据。React 知道这个更新是低优先级的。
  • 第二种写法,tableData不是 useState 保存的值,而是每次渲染时通过useMemo根据deferredKeyword现场计算出来的。React 并不知道具体哪个状态更新是低优先级的,它只负责延迟渲染那个依赖了deferredKeyword的部分。

4.3 关于 isPending 和 isStale 的取舍

useTransitionisPending是它很实用的一个特性,在过渡更新进行中你可以给用户一个视觉反馈(比如转圈或者“加载中”的提示)。这在实际项目里很好用——用户点击 Tab 切换时,新 Tab 内容较大,你可以用isPending显示一个 loading 占位。

useDeferredValue没有直接暴露这个状态。在 React 18 的版本里,你要判断当前渲染的是旧值还是新值,得自己去对比:

// 手动判断 deferredQuery 是否落后于 query const isStale = query !== deferredQuery;

React 19 之后,useDeferredValue会在返回的数组形式中支持isStale字段,但这个能力在 18 里还是需要自己解决的。如果你确实需要 pending 状态,useTransition用起来会更顺手。

4.4 它们可以叠加使用

这两者可以是协作关系。一个需求里,你可能先用startTransition把某个 setState 标记为过渡更新,而在渲染这个过渡状态时,又用useDeferredValue延迟某个具体的值。我很早之前做的数据大屏项目里就遇到过这种场景:切换城市 Tab 时,用startTransition标记城市切换这个更新为过渡,让 Tab 先亮起来、图表数据慢慢换;图表内部,又把时间维度参数useDeferredValue延迟,防止拖动时间轴时图表频繁重新绘制。两个 API 各管一摊,互不冲突。

5. 实际开发中怎么选:场景与踩坑指南

5.1 优先选 useTransition 的场景

我自己在实际开发中,遇到下面这些情况会优先考虑useTransition

一是你在一个事件回调里要更新多个状态,这些状态里只有部分是需要立即反馈的。比如点击“下一页”时,页码和列表数据可以一起更新,但想要页码立刻变、列表数据慢一点刷新,就可以在startTransition里只包住列表数据那个 setState。

二是你依赖isPending来给出过渡提示。这种体验上的细腻差异很重要,用户知道系统在加载,而不是干等着。

三是你的更新来自事件处理函数内部,你可以在回调里清楚地控制哪些更新要优先级高、哪些更新优先级低。这种“手动可控”的感觉,在复杂交互中是很有价值的。

5.2 优先选 useDeferredValue 的场景

反过来,遇到下面这些场景,我会选useDeferredValue

第一种是“值驱动渲染”的典型模式——数据请求已经通过 props 传进来了,你没有机会在一个事件回调里去标记 setState 优先级。这时候直接在渲染层把值延迟一下,效果就来了,改造成本极低。

第二种是你有一个高频变化的状态(比如搜索框的受控值),而这个值被多个昂贵渲染的组件使用。你不想去逐个梳理到底哪个 setState 该包裹、哪个不该包裹,用useDeferredValue把所有基于这个值的渲染统一降级最省事。

第三种是你在组合式组件里,父组件已经把 onQueryChange 之类的回调传下去了,子组件不能随便改事件逻辑。这时候在子组件渲染层用useDeferredValue包装一下收到的新值,伪装成“延迟渲染版本”,非常优雅。

5.3 生产环境里的几个高频坑

这里分享几个我从实际项目里踩出来的坑,希望能帮你少走弯路。

坑一:useDeferredValue 搭配 useMemo 时,dependency 写错

前面提过,依赖必须写deferredQuery而不是query。写错之后,延迟机制完全失效,每次输入都全量计算,页面照样卡。这是新手最容易犯的错。

坑二:startTransition 包裹了非状态更新逻辑

很多人以为startTransition可以包裹任意耗时逻辑,比如在里面对大数组做排序、做 localStorage 写入等。但startTransition只约束 React 的状态更新优先级,它不能把一段同步的 CPU 密集计算变成异步。如果你在startTransition回调里同步跑一段 200ms 的循环,主线程照样会被阻塞。正确的做法是:耗时的数据计算放在useMemouseDeferredValue的渲染链路里,让 React 调度器有机会中断渲染;如果计算本身在事件回调里无法避免,建议配合requestIdleCallback或 Web Worker 来处理。

坑三:把 useDeferredValue 用在受控组件的 value 上

如果在 input 的 value 上直接使用useDeferredValue,那么输入框会“慢半拍”,用户体验会非常奇怪——你敲字符,过一会才出现。受控组件的 value 必须是紧急更新,或者是同步的最新值。useDeferredValue应该用在派生数据的渲染上,而不是用户交互的源头值上。

坑四:过度使用导致两帧渲染合并

useDeferredValue会带来额外的渲染:先渲染旧值,再渲染新值,这可能意味着你的列表组件被渲染两次。如果你在列表组件里有useEffect做昂贵的副作用,容易造成副作用执行两次。建议把副作用改成基于数据状态变化才触发的方式,或者保证副作用执行是幂等的。React 17 时代很多人习惯在useEffect里打点统计或者发请求,到了 18 的并发模式里,这些逻辑如果不加判断,很容易出现重复请求。

5.4 简单选择决策表

为了让决策更清晰,我总结一个固定流程帮你快速判断:

你的需求推荐选择
在事件回调里有多个 setState,需要精细控制优先级useTransition
某个值驱动了大量耗时的渲染计算useDeferredValue
需要 isPending 做 UI 过渡提示useTransition
不能轻易修改事件回调逻辑(比如第三方组件)useDeferredValue
数据已经通过 props 进入组件,想在渲染层降级useDeferredValue
受控输入框 + 大列表过滤,想保留输入框流畅都可以,看你是想控制 setState 还是控制值
需要同步知道当前渲染是否是旧值useTransition(isPending)或用 query !== deferredQuery 判断

5.5 简单聊下 React 19 之后的走向

React 19 里,useTransition的使用方式有了一些增强,比如可以直接配合 action 使用返回的 pending 状态,以及useActionState的引入,在处理表单和 server action 时的体验更顺滑。useDeferredValue也开始支持返回isStale状态位的数组形式。这些变化让两者的分工更清晰了:useTransition越来越偏向“动作级别的过渡”,useDeferredValue则保持“值级别的延迟”。我把话放这里:只要你能理解它们是两个不同层面的优化工具,在 React 19 里这套心智模型依然适用,不需要推倒重来。


真到项目里写的时候,我自己的判断流程其实很简单:先问自己一个问题——“我想让什么东西变慢?”如果答案是“某个 setState 触发的更新”,用useTransition;如果答案是“某个值驱动的渲染派生”,用useDeferredValue。把这个问题想清楚,代码自然就不会写错了。最后还是那句话,React 18 的并发特性并不是让你把所有更新都标记成低优先级,而是让你根据用户感知,主动设计哪些操作值得等待、哪些操作不值得等待。这才是useDeferredValueuseTransition背后真正想让你学会的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 4:16:58

机载LiDAR数据处理全流程:从POS解算到DEM生成的关键技术解析

简介&#xff1a;一份系统讲解机载激光雷达组成与数据处理流程的PPT课件&#xff0c;适合测绘、电力、林业、环境监测等领域的初学者、相关专业学生及教学培训使用。课件围绕LiDAR基本工作原理展开&#xff0c;不仅介绍了激光雷达设备、GPS/IMU定位定姿系统、数据记录系统等硬件…

作者头像 李华
网站建设 2026/9/19 4:15:12

纵横交叉算法优化神经网络结合小波变换的电力负荷预测方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 4:13:43

ResNet18+LSTM活体检测实战:基于OULU-NPU视频时序建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 4:13:27

比ES快5倍的搜索引擎:MeiliSearch轻量级搜索实战指南

这些年做后端&#xff0c;最常被业务方问的一句话就是&#xff1a;“数据量也不大&#xff0c;为什么搜索这么慢&#xff1f;”大多数时候问题不在数据量&#xff0c;而在搜索引擎选型。Elasticsearch确实是搜索界的扛把子&#xff0c;分布式、PB级、聚合分析、日志检索&#x…

作者头像 李华