React 的并发能力不再只是框架内部的实现细节,而是复杂交互体验的核心工具。搜索、筛选、拖拽、富表单和数据面板都可能在同一时间触发大量更新,团队需要明确哪些更新必须立即完成,哪些更新可以延后。2026 年,并发模式已经从"可选的实验特性"变成了 React 应用的默认行为。
渲染优先级要服务用户意图
并发 UI 的关键不是让所有渲染都更快,而是让用户感知到重要操作更稳定。React 的调度模型将更新按优先级分为几类:
紧急更新(Urgent Updates) 需要立即响应用户操作的更新。典型场景包括:
- 输入框的 typing 反馈
- 按钮的 click 状态变化
- 焦点切换和键盘导航
- 拖拽操作的实时位置更新
这些更新应该使用默认的同步渲染,不要包在 useTransition 或 useDeferredValue 里。
过渡更新(Transition Updates) 可以延迟但需要保持一致的更新。React 18 引入的 useTransition 和 startTransition 是这类更新的核心工具:
- 搜索结果列表的渲染
- 筛选条件变化后的列表重排
- Tab 切换后的内容区更新
- 图表和图表的重新计算
把重渲染标记为 transition 后,React 会在紧急更新处理完之后再执行它们,并且如果期间有新的用户操作,会自动中断当前的 transition。
延迟更新(Deferred Updates) 可以进一步延迟、甚至在下一帧才需要的更新。useDeferredValue 适用于:
- 大量数据的列表渲染(虚拟列表之外的场景)
- 非关键的数据面板刷新
- 后台统计数据的更新
一个实用的规则:如果你不确定一个更新是否应该延迟,先把它包在 startTransition 里,然后在 React DevTools Profiler 中观察它是否阻塞了用户交互。
Suspense 边界的艺术
Suspense 可以让加载状态更可控,但边界的选择直接影响用户体验。
边界不能太碎: 如果每个小组件都是一个独立的 Suspense 边界,页面会出现大量局部加载动画(spinner),用户在等待时看到的是闪烁的碎片而不是一个整体。这比一次性加载完整体页面更让人不适。
边界不能太粗糙: 如果整个页面只有一个 Suspense 边界,那慢的组件会阻塞快的组件,一个数据慢会导致整个页面都在 loading。
推荐的边界策略:按用户任务划分
- 页面骨架层:最外层 Suspense,包裹整个路由页面,用于整体加载。fallback 显示页面骨架屏。
- 功能区域层:为独立的用户任务区域设置 Suspense。比如一个 Dashboard 页面上,"筛选面板"、"数据表格"、"图表区"、"推荐侧栏"各自是独立的 Suspense 边界——用户可以先生成筛选条件,再等数据加载。
- 组件级:只在确有必要时才使用。一个参考标准:当某个组件的加载时间超过 2 秒且用户可能在它加载期间操作其他区域时,才值得为它单独设置 Suspense 边界。
一个容易忽视的细节:Suspense 的 fallback 设计。好的 fallback 应该是当前区域的结构占位,而不是一个转圈的 spinner。用骨架屏代替 spinner,可以大幅减少用户的等待焦虑。
useTransition 和 useDeferredValue 的选择
这两个 Hook 看起来相似,但适用场景不同:
使用 useTransition 当更新由用户操作触发:
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleInput = (e) => {
// 高优先级:立即更新输入框的值
setQuery(e.target.value);
// 低优先级:搜索结果可以等
startTransition(() => {
setSearchResults(searchData(e.target.value));
});
};
return (
<>
<input value={query} onChange={handleInput} />
{isPending && <SmallSpinner />}
<SearchResults results={searchResults} />
</>
);
}
使用 useDeferredValue 当更新来自外部数据源:
function Dashboard({ serverData }) {
// 当 serverData 变化时,可以延迟旧页面的渲染
const deferredData = useDeferredValue(serverData);
return (
<div style={{ opacity: serverData !== deferredData ? 0.5 : 1 }}>
<HeavyChart data={deferredData} />
</div>
);
}
关键区别:useTransition 给你 isPending 标志(知道什么时候在等待),useDeferredValue 给你新值和旧值的对比(可以同时展示旧数据并把新数据标记为 stale)。
并发模式下的性能诊断
React 2026 的性能诊断已经从"看渲染次数"升级到了"看交互链路"。三个工具各司其职:
React DevTools Profiler:
- 看组件为什么渲染(props 变化、state 变化、context 变化、hooks 变化)
- 看每个 commit 的耗时
- 识别不必要的重新渲染
Chrome Performance 面板:
- 看主线程的长任务分布
- 看 React 调度器的工作模式(是否正确地使用时间切片)
- 看交互事件的响应延迟(从用户点击到浏览器处理)
RUM 数据(Web Vitals):
- INP(Interaction to Next Paint):2026 年最重要的交互指标,替代了 FID
- 按页面、设备和地区分组的 P75 和 P95 数据
- 发布前后的对比数据
这三类数据要放在一起看:Profiler 告诉你"这个组件渲染了 5 次",Performance 面板告诉你"这 5 次渲染占用了 200ms 主线程",RUM 告诉你"这个页面的 P95 INP 是 180ms"。三者结合,才能判断是否值得优化。
常见反模式
反模式 1:把一切包在 transition 里 不是所有非紧急更新都需要 transition。如果一个更新的计算量很小(例如切换一个布尔值),把它放进 transition 反而增加了复杂度。
反模式 2:在 transition 里做副作用startTransition 只应该包含状态更新。如果在里面发了网络请求、写了 localStorage、触发了埋点,React 中断 transition 时状态回滚了但副作用已经执行了——这会制造诡异的 bug。
反模式 3:用 useMemo/useCallback 代替并发特性 并发模式解决的是"渲染什么时候发生"的问题,memoization 解决的是"渲染什么内容"的问题。两者互补,不能互相替代。
小结
React 2026 的并发实践,本质是在复杂界面中重新分配计算资源。把高优先级交互(用户正在操作的内容)保护好,把低优先级渲染(用户可以等待的内容)安排在浏览器空闲时间执行。关键在于理解三个工具的分工:useTransition 用于用户触发的延迟更新,useDeferredValue 用于外部数据驱动的延迟渲染,Suspense 用于按用户任务划分加载边界。做好这三件事,就能让复杂的 React 应用在交互体验上脱胎换骨。
