关注新特性,渐进式拥抱新特性,可不敢一次直接冲啊!
0. 核心思想速读
- 并发渲染(Concurrent Rendering) :让渲染可中断、可恢复、可批处理,以更平滑的交互取代“卡一下再显示”的体验。
- 优先级调度:React 会给不同更新赋予不同优先级,高优先(输入、动画)可打断低优先(数据渲染) 。
- 可控的“重要/不重要”更新:通过
startTransition
/useTransition
/useDeferredValue
把不影响即时交互的更新“降级”,让 UI 不卡顿。 - 更激进的批处理:自动批处理跨事件源(微任务/宏任务/原生事件)的多次
setState
,减少无谓渲染。 - SSR 全链路升级:支持流式 SSR(Streaming)与选择性水合(Selective Hydration) ,慢数据不阻塞快数据。
1. 并发渲染入口:createRoot
(取代 ReactDOM.render
)
做什么:启用并发渲染能力的入口。
为何重要:只有 createRoot
才能使用 React 18 的并发调度、自动批处理、Transitions 等完整能力。
// index.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootEl = document.getElementById("root")!;
createRoot(rootEl).render(<App />); // ✅ 并发能力开启
友情建议:迁移时先无改动切换到 createRoot,观察告警与行为差异,再逐步引入并发特性。
2. 自动批处理(Automatic Batching)
做什么:在更多场景下把多个 setState
合并为一次渲染。
收益:减少重复渲染,提升吞吐量。
变化点:React 18 前仅在 React 事件中批处理;React 18 起默认跨事件源批处理(例如 setTimeout
、Promise then)。
function DemoAutoBatching() {
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);
function handleClick() {
setTimeout(() => {
setA((x) => x + 1);
setB((x) => x + 1);
// React 18:此处也会自动批处理 -> 只触发一次渲染
}, 0);
}
return (
<div>
<button onClick={handleClick}>Update A & B</button>
<p>{a} - {b}</p>
</div>
);
}
易错点:极少数场景你需要立即同步刷新(比如读取 DOM 布局或第三方同步计算),请用
flushSync
包裹。
import { flushSync } from "react-dom";
flushSync(() => setCount((c) => c + 1));
// 此回调内更新会同步生效,避免布局/测量错位
3. 让重活“不挡路”:startTransition
/ useTransition
做什么:把不紧急的更新标记为“过渡”(Transition),允许被更高优先级任务打断。
典型场景:搜索输入、筛选列表、切 Tab、路由切换——输入即时响应,重算/重渲染后台慢慢做。
3.1 命令式:startTransition
import { startTransition, useState } from "react";
function SearchBox({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const [result, setResult] = useState<string[]>([]);
function onInput(e: React.ChangeEvent<HTMLInputElement>) {
const q = e.target.value;
setQuery(q); // 紧急更新:输入框必须“秒回显”
startTransition(() => {
// 非紧急:重算过滤结果可被打断
const filtered = items.filter((x) => x.includes(q));
setResult(filtered);
});
}
return (
<>
<input value={query} onChange={onInput} placeholder="Type to search..." />
<ul>{result.map((r) => <li key={r}>{r}</li>)}</ul>
</>
);
}
3.2 声明式:useTransition
import { useState, useTransition } from "react";
function BigList({ data }: { data: number[] }) {
const [filter, setFilter] = useState("");
const [list, setList] = useState<number[]>(data);
const [isPending, startTransition] = useTransition();
function onFilterChange(e: React.ChangeEvent<HTMLInputElement>) {
const v = e.target.value;
setFilter(v);
startTransition(() => {
// 慢更新:允许中断
setList(data.filter((x) => String(x).includes(v)));
});
}
return (
<>
<input value={filter} onChange={onFilterChange} />
{isPending && <span>筛选中…</span>}
<ul>{list.map((n) => <li key={n}>{n}</li>)}</ul>
</>
);
}
友情建议:只将“可等待”的 UI 放进 Transition。输入光标、按钮点击反馈等必须即时响应的不要降级。
4. “推迟昂贵依赖”:useDeferredValue
做什么:让某个计算昂贵或渲染昂贵的值延后同步,保留旧值以保证 UI 顺滑。
相比 useTransition
:useTransition
控制“触发时机”;useDeferredValue
控制“消费时机”。
import { useDeferredValue, useMemo, useState } from "react";
function DeferredList({ rawItems }: { rawItems: string[] }) {
const [q, setQ] = useState("");
const deferredQ = useDeferredValue(q); // 让昂贵计算“吃旧值”,先不卡输入
const filtered = useMemo(() => {
// 假设这是昂贵计算
for (let i = 0; i < 2e6; i++) {}
return rawItems.filter((x) => x.includes(deferredQ));
}, [rawItems, deferredQ]);
return (
<>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<p>立即显示的关键词:{q}</p>
<p>稍后用于计算的关键词:{deferredQ}</p>
<ul>{filtered.map((x) => <li key={x}>{x}</li>)}</ul>
</>
);
}
实战定位:
useDeferredValue
更像渲染层面的节流,适合昂贵子树(图表、虚拟表格、语法高亮)而不适合必须实时同步的区域。
5. Suspense
与并发:让“等待”变优雅(含 SSR)
做什么:在组件粒度上为“加载中”提供占位,辅以并发能力实现选择性水合与流式渲染。
客户端常见用法:配合代码分割或数据请求框架(如 React Router、Next.js、TanStack Query)用。
const UserPanel = React.lazy(() => import("./UserPanel"));
export default function App() {
return (
<React.Suspense fallback={<div>加载用户面板…</div>}>
<UserPanel />
</React.Suspense>
);
}
SSR 升级点(概念)
- Streaming SSR:服务端按块输出 HTML,快的先到、慢的后到,首屏更快。
- Selective Hydration:某些组件先水合、某些组件后水合,不必“一起堵车”。
友情建议:在客户端控制好 Suspense 边界大小(过大边界会放大“骨架屏”区域),在 SSR 侧用框架内置支持(Next.js/Remix)获得“流式 + 选择性水合”。
6. 新 Hook 与新契约
6.1 useId
:无冲突、SSR 友好的稳定 ID
场景:aria-*
关联、表单 label
/input
、SSR 与 CSR 需要一致 ID。
function Field({ label }: { label: string }) {
const id = React.useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
}
注意:不要把
useId
结果持久化到业务数据(它只在渲染层稳定,不保证跨进程/存储稳定)。
6.2 useSyncExternalStore
:外部状态的一致读写协议
动机:确保并发渲染下对外部 store(Redux、Zustand、自研)一致快照与可订阅。
// 极简自研 store
const store = (() => {
let value = 0;
let subs = new Set<() => void>();
return {
getSnapshot: () => value,
subscribe: (cb: () => void) => (subs.add(cb), () => subs.delete(cb)),
set: (v: number) => { value = v; subs.forEach((s) => s()); },
};
})();
function Counter() {
const v = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
return (
<>
<div>value: {v}</div>
<button onClick={() => store.set(v + 1)}>+1</button>
</>
);
}
友情建议:库作者请用它封装对外部状态的订阅;应用层若使用成熟状态库,升级到支持
useSyncExternalStore
的版本即可。
6.3 useInsertionEffect
:样式注入时机(CSS-in-JS 场景)
- 在 DOM 变更前运行,保证样式优先插入,避免 FOUC(闪烁)。
- 仅限样式系统/库作者;业务代码尽量避免使用。
function StyleInjector({ css }: { css: string }) {
React.useInsertionEffect(() => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
return () => style.remove();
}, [css]);
return null;
}
7. 并发友好型性能策略(把特性用“对位”了)
- 输入优先:输入框/拖拽/滚动等即时交互保持非过渡更新;数据过滤/渲染改用
startTransition
。 - 昂贵子树隔离:对图表/虚拟列表等耗时组件,使用
useDeferredValue
推迟依赖或在父级Suspense
包裹骨架。 - 避免可中断副作用:数据拉取/副作用不要依赖可能被中断的渲染快照,放入事件或 effect,并做好幂等与取消。
- 监控优先渲染路径:以“输入 → 响应”链路为第一优先级优化目标,评估 FPS、TTI、INP(交互到下一绘制)。
- SSR 边界划分:在可控粒度上设计
Suspense
边界,对慢资源单独边界,减少“整页骨架”。
8. 迁移与稳定性(真实工程中的“坑位图”)
- StrictMode(开发环境) :某些 effect 会执行两次(意在发现不安全副作用)。生产不会;但你要保证副作用可重入/可取消。
- 自动批处理行为改变:老代码里对“每次 setState 都渲染一次”的隐性假设要移除;实在需要同步渲染时用
flushSync
。 - 第三方库升级:确保状态管理库(Redux、Zustand)、样式库(styled-components、emotion)已适配
useSyncExternalStore
/useInsertionEffect
。 - SSR 框架版本:使用 Next.js/Remix 等的 React 18 适配版本,开启 streaming/hydration 才能吃满红利。
9. 综合示例:丝滑搜索面板(输入秒回显 + 重渲染可打断 + 昂贵列表延后)
import React from "react";
import { createRoot } from "react-dom/client";
type Item = { id: number; title: string; body: string };
function useFakeData(size = 50_000): Item[] {
return React.useMemo(
() => Array.from({ length: size }, (_, i) => ({
id: i, title: `Item ${i}`, body: "x".repeat(200)
})),
[size]
);
}
function App() {
const data = useFakeData();
const [q, setQ] = React.useState("");
const deferredQ = React.useDeferredValue(q); // 昂贵列表吃“旧值”
const [filtered, setFiltered] = React.useState<Item[]>(data);
const [isPending, startTransition] = React.useTransition();
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setQ(v); // 紧急:输入“秒回显”
startTransition(() => {
// 非紧急:允许被打断的重算
const res = data.filter((it) =>
it.title.includes(v) || it.body.includes(v)
);
setFiltered(res);
});
};
const expensiveList = React.useMemo(() => {
// 模拟昂贵渲染:巨量节点 + 计算
const heavy = [];
for (let i = 0; i < 3e6; i++) {} // burn CPU
for (const it of filtered) heavy.push(<li key={it.id}>{it.title}</li>);
return heavy;
}, [filtered, deferredQ]); // 注意:使用 deferredQ 可在某些场景下进一步缓和波动
return (
<div>
<h3>丝滑搜索</h3>
<input value={q} onChange={onChange} placeholder="键入关键字…" />
{isPending && <span style={{ marginLeft: 8 }}>计算中…</span>}
<ul>{expensiveList}</ul>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);
为何顺滑?
- 输入框用紧急更新,立刻回显;
- 重计算放入 Transition,可被后续输入打断;
- 昂贵渲染依赖
deferredQ
,消费延后进一步平滑。
10. 学习与落地清单(从“会用”到“用得对”)
- 全量切换到
createRoot
,清理ReactDOM.render
- 在“输入→列表”路径引入
startTransition
- 对图表/虚拟表格引入
useDeferredValue
- Store 订阅层升级到
useSyncExternalStore
- CSS-in-JS 升级到支持
useInsertionEffect
- 客户端拆
Suspense
边界;SSR 打开 Streaming + 选择性水合 - 建立 INP/FPS 监控面板,度量“交互 → 下一帧”质量
- 严格检查副作用可重入性(适配 StrictMode)
总结
React 18 的“并发”不是“并行”,而是“可中断、可恢复、可分级”的渲染范式。掌握 startTransition / useDeferredValue / Suspense + Streaming
的正确边界,再配合自动批处理与外部状态协议,你在复杂/大规模前端里的体验优化与稳定性治理会直接上一个台阶。