在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- React 18 并发渲染的底层原理和工作机制
useTransition和useDeferredValue的实战应用场景- 如何使用
Suspense优化数据加载体验 - 自动批处理带来的性能提升
- 大型列表和复杂交互场景的优化方案
在大型 React 应用中,我们经常遇到这样的场景:用户在搜索框输入时,页面需要同时更新输入框内容和下方的搜索结果列表。传统的同步渲染会导致输入框卡顿,用户体验极差。React 18 引入的并发特性(Concurrent Features)从根本上解决了这个问题,它允许 React 中断、暂停和恢复渲染工作,让高优先级的更新(如用户输入)能够立即响应。通过本文,你将掌握如何在实际项目中应用这些特性,显著提升应用的交互性能和用户体验。
一、React 18 并发特性概述
React 18 最重要的更新就是引入了并发渲染(Concurrent Rendering)能力。与 React 17 的同步渲染不同,并发渲染允许 React 同时准备多个版本的 UI,并根据优先级决定哪些更新应该先完成。
核心概念理解
并发渲染并不是指多线程或并行执行,而是指 React 可以在渲染过程中被中断。想象一下,你在写一份文档,突然有紧急电话打来,你可以先放下手头工作去接电话,之后再继续写文档。React 18 的并发特性就是这样工作的。
在 React 17 中,一旦开始渲染,就必须完成整个组件树的渲染才能响应用户交互。这在处理大型列表或复杂计算时会导致明显的卡顿。React 18 通过时间切片(Time Slicing)技术,将渲染工作分解成多个小任务,在每个任务之间检查是否有更高优先级的工作需要处理。
并发特性的核心 API
React 18 提供了几个关键的 API 来使用并发特性:
-
useTransition:标记非紧急的状态更新,允许它们被中断 -
useDeferredValue:延迟更新某个值,保持 UI 的响应性 -
Suspense:配合并发渲染优化数据加载体验 -
自动批处理(Automatic Batching):自动合并多个状态更新
这些 API 不需要你重写现有代码,它们是渐进式的增强功能。你可以在需要优化的地方逐步引入。

二、自动批处理:无需改动的性能提升
自动批处理是 React 18 中最容易获得收益的特性,因为它完全自动工作,不需要任何代码修改。
React 17 的批处理限制
在 React 17 中,只有在事件处理函数内部的多个状态更新会被批处理。但在 Promise、setTimeout 或原生事件处理中,每个状态更新都会触发一次重新渲染。
javascript
// React 17 中的行为
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次重新渲染 ✅
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 触发两次重新渲染 ❌
}, 1000);
这种不一致的行为不仅让人困惑,还会导致不必要的性能损耗。
React 18 的自动批处理
React 18 将批处理扩展到所有场景,无论状态更新发生在哪里,都会自动合并。
javascript
// React 18 中的行为
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次重新渲染 ✅
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次重新渲染 ✅(React 18 的改进)
}, 1000);
fetch('/api/data').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次重新渲染 ✅
});
实测性能提升
在我们的生产项目中,仅仅升级到 React 18 并启用自动批处理,就让某些页面的渲染次数减少了 30%-40%。特别是在处理异步数据更新的场景中,效果最为明显。
如果你确实需要同步更新(极少数情况),可以使用 flushSync:
javascript
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1);
});
// DOM 已经更新
flushSync(() => {
setFlag(f => !f);
});
// DOM 再次更新
但在实际开发中,几乎不需要使用 flushSync。自动批处理的默认行为已经足够智能。
三、useTransition:区分紧急和非紧急更新
useTransition 是并发特性中最实用的 Hook,它允许你将某些状态更新标记为"非紧急",从而保持 UI 的响应性。
典型使用场景
最常见的场景是搜索功能。用户在输入框中输入时,我们需要:
-
立即更新输入框的值(紧急)
-
根据输入过滤大量数据(非紧急)
javascript
import { useState, useTransition } from 'react';
function SearchList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [filteredItems, setFilteredItems] = useState(items);
function handleChange(e) {
const value = e.target.value;
setQuery(value); // 紧急更新:立即更新输入框
startTransition(() => {
// 非紧急更新:可以被中断的过滤操作
const filtered = items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
}
return (
<div>
<input value="{query}" onchange="{handleChange}" placeholder="搜索...">
{isPending && <span>搜索中...</span>}
<ul style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
{filteredItems.map(item => (
<li key="{item.id}">{item.name}</li>
))}
</ul>
</div>
);
}
工作原理详解

useTransition 返回两个值:
-
isPending:布尔值,表示是否有待处理的 transition -
startTransition:函数,用于包裹非紧急的状态更新
当用户快速输入时,React 会:
-
立即执行
setQuery,更新输入框 -
开始执行
startTransition中的过滤逻辑 -
如果用户继续输入,中断之前的过滤工作,开始新的过滤
-
只渲染最后一次的过滤结果
这种机制避免了无用的中间状态渲染,显著提升了性能。
真实项目经验
在我们的电商项目中,商品列表有 5000+ 条数据。使用 useTransition 之前,用户输入时会有明显的卡顿,输入框延迟达到 200-300ms。引入 useTransition 后,输入框始终保持流畅,延迟降低到 16ms 以内(一帧的时间)。
需要注意的是,startTransition 中的更新必须是状态更新,不能包含异步操作:
javascript
// ❌ 错误:不能在 startTransition 中使用 async
startTransition(async () => {
const data = await fetchData();
setData(data);
});
// ✅ 正确:异步操作在外部,只把状态更新放在 startTransition 中
const data = await fetchData();
startTransition(() => {
setData(data);
});
四、useDeferredValue:延迟更新派生状态
useDeferredValue 是另一个处理非紧急更新的 Hook,它更适合处理派生状态的场景。
与 useTransition 的区别
-
useTransition:你控制何时触发非紧急更新 -
useDeferredValue:React 自动延迟某个值的更新
javascript
import { useState, useDeferredValue, memo } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<input value="{query}" onchange="{e" ==""> setQuery(e.target.value)}
/>
<searchresults query="{deferredQuery}">
</searchresults></div>
);
}
// 使用 memo 避免不必要的重新渲染
const SearchResults = memo(function SearchResults({ query }) {
const items = useSearchItems(query); // 耗时的搜索逻辑
return (
<ul>
{items.map(item => (
<li key="{item.id}">{item.name}</li>
))}
</ul>
);
});
工作机制
当 query 快速变化时:
-
query立即更新,输入框保持响应 -
deferredQuery延迟更新,等待 React 有空闲时间 -
SearchResults使用延迟的deferredQuery,不会阻塞输入
useDeferredValue 特别适合配合 memo 使用。因为 deferredQuery 变化频率低,SearchResults 的重新渲染次数也会减少。
选择 useTransition 还是 useDeferredValue?
根据我的实践经验:
-
**使用 **
useTransition:当你能直接控制状态更新的代码时-
表单提交、按钮点击等明确的用户操作
-
需要显示 loading 状态(
isPending)
-
-
**使用 **
useDeferredValue:当你只能接收一个值,无法控制其更新时-
接收 props 传入的值
-
需要对某个值进行昂贵的派生计算
-
更简洁的代码,不需要手动包裹
startTransition
-
实际项目中,两者可以组合使用:
javascript
function ParentComponent() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(value) {
setQuery(value); // 立即更新
startTransition(() => {
// 触发其他非紧急更新
updateAnalytics(value);
});
}
return <childcomponent query="{query}" ispending="{isPending}">;
}
function ChildComponent({ query, isPending }) {
const deferredQuery = useDeferredValue(query);
// 使用 deferredQuery 进行渲染
}
</childcomponent>
五、Suspense:优化数据加载体验
Suspense 在 React 18 中得到了增强,配合并发渲染能提供更好的加载体验。
基础使用
javascript
import { Suspense } from 'react';
function App() {
return (
<suspense fallback="{<LoadingSpinner">}>
<userprofile>
<postlist>
</postlist></userprofile></suspense>
);
}
当 UserProfile 或 PostList 在加载数据时,会显示 LoadingSpinner。数据加载完成后,自动切换到实际内容。
配合 React Query 使用
在实际项目中,Suspense 通常配合数据获取库使用。以 React Query 为例:
javascript
import { Suspense } from 'react';
import { useQuery } from '@tanstack/react-query';
function UserProfile() {
// 启用 suspense 模式
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
suspense: true,
});
return <div>{data.name}</div>;
}
function App() {
return (
<suspense fallback="{<Skeleton">}>
<userprofile>
</userprofile></suspense>
);
}
嵌套 Suspense 实现渐进式加载
更高级的用法是使用嵌套的 Suspense,实现不同部分的独立加载:
javascript
function Dashboard() {
return (
<div>
<suspense fallback="{<HeaderSkeleton">}>
<header>
<suspense fallback="{<ChartSkeleton">}>
<charts>
</charts></suspense>
<suspense fallback="{<TableSkeleton">}>
<datatable>
</datatable></suspense>
</header></suspense></div>
);
}
这样,Header 加载完成后会立即显示,不需要等待 Charts 和 DataTable。用户能更快看到页面内容,体验更流畅。
配合 useTransition 避免闪烁
直接使用 Suspense 可能会导致页面闪烁(显示 loading → 显示内容 → 又显示 loading)。配合 useTransition 可以解决这个问题:
javascript
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onclick="{()" ==""> selectTab('home')}>首页</button>
<button onclick="{()" ==""> selectTab('profile')}>个人中心</button>
{isPending && <spinner>}
<suspense fallback="{<PageSkeleton">}>
{tab === 'home' && <homepage>}
{tab === 'profile' && <profilepage>}
</profilepage></homepage></suspense>
</spinner></div>
);
}
使用 startTransition 包裹标签切换后,React 会:
-
保持显示当前标签的内容
-
在后台准备新标签的内容
-
只有在新内容准备好后才切换,避免显示 loading
这种模式在我们的后台管理系统中广泛使用,用户切换标签时几乎感觉不到加载过程。
六、并发渲染的工作原理
理解并发渲染的底层原理,能帮助你更好地使用这些 API。
React 17 vs React 18 的渲染差异

React 17 同步渲染:
-
渲染开始后必须完成整个组件树
-
渲染期间无法响应用户交互
-
长时间渲染会导致页面卡顿
React 18 并发渲染:
-
渲染可以被中断和恢复
-
高优先级更新可以插队
-
保持 UI 始终响应用户操作
优先级调度机制
React 18 内部使用 Scheduler 来管理任务优先级。不同类型的更新有不同的优先级:
-
同步优先级 :
flushSync中的更新,立即执行 -
用户交互优先级:点击、输入等用户操作,高优先级
-
默认优先级:普通的状态更新
-
Transition 优先级 :
startTransition中的更新,低优先级 -
空闲优先级:可以无限延迟的更新
当多个更新同时发生时,React 会:
-
先执行高优先级更新
-
渲染高优先级更新的结果
-
在浏览器空闲时继续执行低优先级更新
时间切片的实现
React 使用 MessageChannel 或 setTimeout 将渲染工作分解成 5ms 的小任务。每个任务执行后,React 会检查:
-
是否有更高优先级的工作?
-
浏览器是否需要处理用户输入或绘制?
如果有,React 会暂停当前工作,让浏览器处理更重要的事情。这就是为什么使用并发特性后,即使在渲染大量组件时,UI 仍然保持响应。
渲染阶段的可中断性
需要注意的是,只有渲染阶段(Render Phase)可以被中断,提交阶段(Commit Phase)仍然是同步的。这保证了:
-
DOM 更新是原子性的,不会出现不一致的状态
-
副作用(
useEffect)按正确的顺序执行
这也意味着,渲染阶段的函数(组件函数、useMemo 等)可能被多次调用,所以它们必须是纯函数,不能有副作用。
七、实战案例:大型列表优化
让我们通过一个完整的案例,看看如何综合运用并发特性优化大型列表。
场景描述
假设我们有一个包含 10,000 条商品的列表,需要支持:
-
实时搜索过滤
-
按类别筛选
-
按价格排序
传统实现会导致严重的性能问题。
优化前的代码
javascript
function ProductList({ products }) {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
// 每次状态变化都会重新计算,阻塞 UI
const filteredProducts = products
.filter(p => p.name.includes(query))
.filter(p => category === 'all' || p.category === category)
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
return (
<div>
<input value="{query}" onchange="{e" ==""> setQuery(e.target.value)}
/>
<select value="{category}" onchange="{e" ==""> setCategory(e.target.value)}>
<option value="all">全部</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
</select>
<select value="{sortBy}" onchange="{e" ==""> setSortBy(e.target.value)}>
<option value="name">按名称</option>
<option value="price">按价格</option>
</select>
<ul>
{filteredProducts.map(p => (
<li key="{p.id}">{p.name} - ¥{p.price}</li>
))}
</ul>
</div>
);
}
这段代码的问题:
-
用户输入时,过滤、排序操作会阻塞输入框
-
每次状态变化都重新计算整个列表
-
没有任何优化措施
优化后的代码
javascript
import { useState, useMemo, useTransition, useDeferredValue } from 'react';
function ProductList({ products }) {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const [isPending, startTransition] = useTransition();
// 使用 deferredValue 延迟搜索词的更新
const deferredQuery = useDeferredValue(query);
// 使用 useMemo 缓存计算结果
const filteredProducts = useMemo(() => {
console.log('重新计算列表'); // 用于调试
return products
.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
)
.filter(p => category === 'all' || p.category === category)
.sort((a, b) => {
if (sortBy === 'price') {
return a.price - b.price;
}
return a.name.localeCompare(b.name);
});
}, [products, deferredQuery, category, sortBy]);
function handleQueryChange(e) {
setQuery(e.target.value); // 立即更新输入框
}
function handleCategoryChange(e) {
startTransition(() => {
setCategory(e.target.value); // 非紧急更新
});
}
function handleSortChange(e) {
startTransition(() => {
setSortBy(e.target.value); // 非紧急更新
});
}
return (
<div>
<div classname="filters">
<input value="{query}" onchange="{handleQueryChange}" placeholder="搜索商品...">
<select value="{category}" onchange="{handleCategoryChange}" disabled="{isPending}">
<option value="all">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
</select>
<select value="{sortBy}" onchange="{handleSortChange}" disabled="{isPending}">
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
</select>
{isPending && <span classname="loading">更新中...</span>}
</div>
<productgrid products="{filteredProducts}" ispending="{isPending}">
</productgrid></div>
);
}
// 将列表渲染抽离成独立组件,配合 memo 优化
const ProductGrid = memo(function ProductGrid({ products, isPending }) {
return (
<ul style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
{products.map(p => (
<productitem key="{p.id}" product="{p}">
))}
</productitem></ul>
);
});
// 单个商品项也使用 memo
const ProductItem = memo(function ProductItem({ product }) {
return (
<li classname="product-item">
<h3>{product.name}</h3>
<p classname="price">¥{product.price}</p>
<span classname="category">{product.category}</span>
</li>
);
});
优化效果对比
在我们的测试中(10,000 条数据):
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 输入延迟 | 250ms | 16ms | 93% ↓ |
| 筛选响应时间 | 300ms | 50ms | 83% ↓ |
| 渲染次数(快速输入 10 个字符) | 10 次 | 2-3 次 | 70% ↓ |
| 首次渲染时间 | 180ms | 180ms | 无变化 |
关键优化点:
-
useDeferredValue延迟搜索词更新,减少过滤计算次数 -
useTransition将筛选和排序标记为非紧急,保持 UI 响应 -
useMemo缓存计算结果,避免重复计算 -
memo避免子组件不必要的重新渲染 -
视觉反馈 使用
isPending提供加载状态
进一步优化:虚拟滚动
对于超大列表(10,000+ 条),即使使用并发特性,渲染所有 DOM 节点仍然会有性能问题。这时可以配合虚拟滚动库(如 react-window 或 react-virtual):
javascript
import { FixedSizeList } from 'react-window';
function ProductGrid({ products, isPending }) {
const Row = ({ index, style }) => (
<div style="{style}">
<productitem product="{products[index]}">
</productitem></div>
);
return (
<fixedsizelist height="{600}" itemcount="{products.length}" itemsize="{80}" width="100%" style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
{Row}
</fixedsizelist>
);
}
虚拟滚动 + 并发特性的组合,可以轻松处理 100,000+ 条数据的列表。
八、最佳实践与注意事项
基于实际项目经验,总结以下最佳实践。
何时使用并发特性
应该使用的场景:
-
搜索框实时过滤大量数据
-
复杂表单的联动计算
-
标签页切换加载数据
-
大型列表的排序和筛选
-
图表数据的实时更新
不需要使用的场景:
-
简单的表单输入(几个字段)
-
小型列表(< 100 条)
-
静态内容展示
-
已经足够快的操作
记住:并发特性是优化工具,不是必需品。不要为了使用而使用。
常见陷阱
1. 在 startTransition 中使用 async/await
javascript
// ❌ 错误
startTransition(async () => {
const data = await fetchData();
setData(data);
});
// ✅ 正确
async function loadData() {
const data = await fetchData();
startTransition(() => {
setData(data);
});
}
2. 忘记使用 memo
useDeferredValue 配合 memo 才能发挥最大效果:
javascript
// ❌ 效果不佳:子组件每次都重新渲染
function Parent() {
const deferredValue = useDeferredValue(value);
return <child value="{deferredValue}">;
}
// ✅ 正确:子组件只在 deferredValue 变化时渲染
const Child = memo(function Child({ value }) {
// ...
});
</child>
3. 过度使用 useTransition
不是所有状态更新都需要 useTransition。只有当更新确实耗时且会阻塞 UI 时才使用。
javascript
// ❌ 没必要
startTransition(() => {
setCount(count + 1); // 简单的计数器不需要
});
// ✅ 有必要
startTransition(() => {
setFilteredList(expensiveFilter(list, query)); // 耗时操作
});
性能监控
使用 React DevTools Profiler 监控并发特性的效果:
-
打开 React DevTools
-
切换到 Profiler 标签
-
点击录制按钮
-
执行你要测试的操作
-
查看渲染时间和次数
关注以下指标:
-
Commit duration:每次提交的耗时
-
Render count:渲染次数
-
Interactions:用户操作的响应时间
兼容性考虑
React 18 的并发特性是可选的,不会破坏现有代码。但需要注意:
-
确保使用
ReactDOM.createRoot而不是ReactDOM.render -
第三方库需要支持并发模式(大部分主流库已支持)
-
严格模式下,组件会被双重调用以检测副作用
javascript
// React 18 的正确启动方式
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<app>);
</app>
调试技巧
当并发特性行为不符合预期时:
-
添加日志 :在组件中添加
console.log查看渲染次数 -
使用 Profiler API:编程方式收集性能数据
-
检查 key:确保列表项有稳定的 key
-
验证纯函数:确保渲染函数没有副作用
javascript
import { Profiler } from 'react';
function onRenderCallback(
id, // 发生提交的 Profiler 树的 "id"
phase, // "mount" (首次挂载) 或 "update" (重新渲染)
actualDuration, // 本次更新花费的渲染时间
baseDuration, // 不使用 memoization 的情况下渲染整棵子树需要的时间
startTime, // 本次更新开始渲染的时间
commitTime, // 本次更新提交的时间
interactions // 本次更新的 interactions 集合
) {
console.log(`${id} ${phase} took ${actualDuration}ms`);
}
<profiler id="ProductList" onrender="{onRenderCallback}"><productlist></productlist></profiler>
总结
React 18 的并发特性是前端性能优化的重要里程碑。通过本文,我们深入学习了:
-
自动批处理让所有场景下的状态更新都能自动合并,无需修改代码即可获得性能提升
-
useTransition 允许我们区分紧急和非紧急更新,保持 UI 始终响应用户操作,特别适合搜索、过滤等场景
-
useDeferredValue 提供了更简洁的方式来延迟派生状态的更新,配合 memo 使用效果最佳
-
Suspense 配合并发渲染能提供更流畅的加载体验,避免页面闪烁
-
并发渲染的底层原理基于优先级调度和时间切片,让 React 能够中断和恢复渲染工作
在实际项目中,建议采用渐进式的方式引入这些特性。先从最明显的性能瓶颈入手,使用 useTransition 或 useDeferredValue 优化,然后逐步扩展到其他场景。记住,并发特性是优化工具,不是必需品,只在真正需要时使用才能发挥最大价值。