【React】 Hooks useTransition 解析与性能优化实践
1.背景
useTransition
是 React 18 引入的一个并发模式下的 Hook,用于区分紧急和非紧急的状态更新,提升应用的响应性和用户体验;- 它可以管理 UI 中的过渡状态,特别是在处理长时间运行的状态更新时。
- 它允许你将某些更新标记为“过渡”状态,这样 React 可以优先处理更重要的更新,比如用户输入,同时延迟处理过渡更新。
- 以下是其核心要点、使用场景和注意事项:
2.核心功能
- 标记过渡状态:将非紧急的状态更新标记为“过渡”(Transition),允许 React 延迟处理这些更新,优先处理高优先级任务(如用户交互)。
- 保持 UI 响应:在状态转换期间,组件仍能保持响应,避免界面卡顿。
- 提供加载状态:通过
isPending
指示过渡任务是否正在进行,可配合加载指示器(如 Spinner)提升用户体验。
3.基本用法
3.1 eg:
import { useTransition } from 'react';function MyComponent() {const [isPending, startTransition] = useTransition();const [count, setCount] = useState(0);const handleClick = () => {startTransition(() => {setCount(c => c + 1); // 标记为过渡更新});};return (<div>{isPending && <Spinner />} {/* 显示加载指示器 */}<button onClick={handleClick}>Increment</button><p>Count: {count}</p></div>);
}
3.1 参数
useTransition
不需要任何参数
3.2 返回值
useTransition
返回一个数组 包含两个元素[isPending, startTransition]
isPending
:布尔值,表示过渡任务是否正在进行。startTransition
:函数,用于包裹低优先级的状态更新。
4、使用场景
4.1 大量数据渲染
例如,过滤或排序大型列表时,避免界面卡顿。
function DataGrid() {const [data, setData] = useState([]);const [isPending, startTransition] = useTransition();const [filter, setFilter] = useState('');const handleFilterChange = (newFilter) => {setFilter(newFilter);startTransition(() => {const filteredData = processLargeDataSet(newFilter);setData(filteredData);});};return (<div><input value={filter} onChange={e => handleFilterChange(e.target.value)} />{isPending ? <LoadingGrid /> : <VirtualizedGrid data={data} />}</div>);
}
4.2 路由切换:
预加载下一页数据时保持当前页响应。
function App() {const [isPending, startTransition] = useTransition();const [currentPage, setCurrentPage] = useState('home');const navigate = (page) => {startTransition(() => {setCurrentPage(page);});};return (<div><Navigation onNavigate={navigate} />{isPending ? <PageTransitionSpinner /> : <Page name={currentPage} />}</div>);
}
4.3 表单验证
实时响应用户输入,同时延迟更新验证结果。
function ComplexForm() {const [formData, setFormData] = useState({});const [errors, setErrors] = useState({});const [isPending, startTransition] = useTransition();const handleChange = (e) => {const { name, value } = e.target;setFormData(prev => ({ ...prev, [name]: value }));startTransition(() => {const validationErrors = validateFormField(name, value);setErrors(prev => ({ ...prev, [name]: validationErrors }));});};return (<form><input name="email" onChange={handleChange} value={formData.email || ''} />{isPending ? <ValidatingIndicator /> : (errors.email && <ErrorMessage error={errors.email} />)}</form>);
}
4.4 搜索或筛选功能
实时响应用户输入,同时延迟更新结果展示。
function SearchResults() {const [query, setQuery] = useState('');const [results, setResults] = useState([]);const [isPending, startTransition] = useTransition();const handleSearch = (e) => {setQuery(e.target.value);startTransition(() => {const searchResults = performSearch(e.target.value);setResults(searchResults);});};return (<div><input value={query} onChange={handleSearch} />{isPending ? <div>Loading...</div> : (<ul>{results.map(result => (<li key={result.id}>{result.title}</li>))}</ul>)}</div>);
}
5. 场景模拟
创建了一个简单的输入框和一个列表,用于展示基于输入关键词的结果。
mockjs文档地址:https://github.com/nuysoft/Mock/wiki/Getting-Started
5.1 安装使用到的插件
pnpm add mockjs antd
pnpm add -D @types/mockjs @type/node
package.json
{...."dependencies": {"antd": "^5.24.9","mockjs": "^1.1.0","react": "^19.0.0","react-dom": "^19.0.0"},"devDependencies": {"@types/mockjs": "^1.0.10","@types/node": "^22.15.3","@types/react": "^19.1.2",.....}
}
5.2 编写 vite.config.ts
结合 vite插件实现一个api, 这个api可以帮助我们模拟数据。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import type { Plugin } from 'vite'
import mockjs from 'mockjs'
// 使用 URL 模块解析 URL
import url from 'node:url'/*** 创建一个 Vite Mock 服务器插件* @returns Vite 插件实例*/
const viteMockServer = (): Plugin => {return {// 插件名称name: 'vite-plugin-mock',// 配置开发服务器configureServer(server) {// 添加中间件处理 mock 请求server.middlewares.use('/api/mock/list', (req, res) => {// 解析请求 URL 中的查询参数const parseurl = url.parse(req.originalUrl ?? '', true).query;// 设置响应头为 JSON 格式res.setHeader('Content-Type', 'application/json');// 使用 mockjs 生成模拟数据const data = mockjs.mock({'list|2000': [{id: '@guid',// name: '@cword(2, 4)',name: parseurl.key,age: '@integer(18, 30)',address: '@county(true)',},],});// 将数据转换为 JSON 字符串并发送响应res.end(JSON.stringify(data));})}}
}// https://vite.dev/config/
export default defineConfig({plugins: [react(), viteMockServer()],
})
编写完成访问我们的接口
http://localhost:5174/api/list?keyWord=xx
5174为默认端口,可以自行更改,返回数据如下{"list": [{"id": "DCe1D11e-8D24-31e6-fE76-5AbAA6cf7E6F","name": "a","age": 25,"address": "西藏自治区 日喀则地区 仲巴县"},{"id": "EAb5ffb6-b43B-93cC-cDfb-18A7Ce15BFda","name": "a","age": 21,"address": "安徽省 马鞍山市 花山区"}.......] }
5.3 业务组件编写
App.tsx
- 输入框和状态管理 使用 useState Hook 管理输入框的值和结果列表。 每次输入框的内容变化时,handleInputChange 函数会被触发,它会获取用户输入的值,并进行 API 请求。
- API 请求 在 handleInputChange 中,输入的值会作为查询参数发送到 /api/list API。API 返回的数据用于更新结果列表。 为了优化用户体验,我们将结果更新放在 startTransition 函数中,这样 React 可以在处理更新时保持输入框的响应性。
- 使用 useTransition useTransition 返回一个布尔值 isPending,指示过渡任务是否仍在进行中。 当用户输入时,如果正在加载数据,我们会显示一个简单的“loading…”提示,以告知用户当前操作仍在进行。
- 列表渲染 使用 List 组件展示返回的结果,列表项显示每个结果的 name 和 address。
// 使用 React 和 Ant Design 组件实现一个模拟带搜索功能的列表
import { Input, List, Spin } from 'antd'
import { useState, useTransition, useCallback } from 'react'interface ListItem {id: string;name: string;address: string;age: number;
}function App() {const [list, setList] = useState<ListItem[]>([]);const [inputValue, setInputValue] = useState('');const [isPending, startTransition] = useTransition();// 处理输入框变化,使用 useCallback 优化性能const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {const value = e.target.value;setInputValue(value);// 发起请求获取数据fetch(`/api/mock/list?key=${value}`).then((res) => res.json()).then((res) => {// 使用 startTransition 包裹状态更新,优化大数据渲染性能startTransition(() => {setList(res.list)});}).catch(err => {console.error('获取数据失败:', err);});}, []);return (<><Input placeholder="请输入搜索关键词"value={inputValue} onChange={handleInputChange} />{isPending ? (<Spin tip="加载中..." />) : (<ListdataSource={list}renderItem={(item) => (<List.Item>{item.address}</List.Item>)}/>)}</>);
}export default App;
5.4 测试使用
为了更好的测试结果可以在性能中降级 cpu 渲染速度
6. 注意事项
startTransition必须是同步的
错误做法
startTransition(() => {// ❌ 在调用 startTransition 后更新状态setTimeout(() => {setPage('/about');}, 1000);
});
正确做法
setTimeout(() => {startTransition(() => {// ✅ 在调用 startTransition 中更新状态setPage('/about');});
}, 1000);
async await 错误做法
startTransition(async () => {await someAsyncFunction();// ❌ 在调用 startTransition 后更新状态setPage('/about');
});
正确做法
await someAsyncFunction();
startTransition(() => {// ✅ 在调用 startTransition 中更新状态setPage('/about');
});
7. 原理剖析
useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:
- 高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。
- 低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。
+-----------------------+| App || || +--------------+ || | Input | || +--------------+ || || +--------------+ || | Display | || +--------------+ |+-----------------------+用户输入|v[高优先级更新] ---> [调度器] ---> [React 更新组件]|+---> [低优先级过渡更新] --> [调度器] --> [等待处理]
8.扩展
useTransition
与防抖的区别?
seTransition
与 防抖在前端开发中都是用于优化性能的手段,但它们的核心原理、应用场景和实现方式存在显著区别,以下是详细对比:
核心原理对比
useTransition
- 基于React的并发模式(Concurrent Features),通过将状态更新标记为“过渡”(Transition)实现。
- 允许React在后台处理低优先级任务,优先响应高优先级操作(如用户输入),从而保持界面流畅。
- 更新过程可中断,避免长时间阻塞主线程,提升用户体验。
- 防抖(Debounce)
- 延迟函数执行,直到事件触发后的一段时间内没有再次触发。
- 适用于需要等待用户停止操作后再执行的场景(如搜索输入、表单验证)。
应用场景对比
useTransition
适用场景- 大数据列表过滤:当用户输入搜索内容时,通过
useTransition
将筛选任务标记为低优先级,避免阻塞输入框的实时更新,保持输入流畅性。 - 复杂UI更新:在渲染需要消耗大量时间的页面时,使用
useTransition
优化视图切换体验。 - 表单提交与筛选器切换:需要延迟渲染的操作(如异步数据加载)中,
useTransition
可确保用户交互的即时响应。
- 大数据列表过滤:当用户输入搜索内容时,通过
- 防抖适用场景
- 搜索框输入检测:等待用户停止输入后再执行搜索,减少无效请求。
- 手机号和邮箱验证:在用户停止输入后验证格式,避免实时验证的性能开销。
- 窗口大小变化后的重新渲染:在窗口调整结束后执行布局计算,避免频繁重绘。
优缺点对比
useTransition
优点- 更新协调过程可中断,避免长时间阻塞主线程。
- 用户操作可及时得到响应,提升交互体验。
- 不需要开发者手动配置时间间隔,React自动优化。
useTransition
缺点- 仅适用于React 18及以上版本,且需配合并发模式使用。
- 对于简单场景,可能引入不必要的复杂性。
- 防抖优点
- 可确保在用户停止操作后执行函数,减少无效请求。
- 实现简单,适用于等待用户输入的场景。
- 防抖缺点
- 可能导致用户输入长时间得不到响应。
- 无法处理需要即时反馈的操作。