适用时机
在以下场景中使用本指南:
- 编写新的 React 组件或页面
- 实现数据获取逻辑
- 审查代码性能问题
- 重构已有代码
- 优化 bundle 大小和加载时间
- 迁移到 React 19+ / Next.js 15+ / Server Components
一、消除请求瀑布(CRITICAL)
请求瀑布是性能的头号杀手。多个异步操作串行执行时,总延迟 = 所有操作之和。
1.1 延迟 await 到真正需要的位置
// ❌ 阻塞整个函数
async function Page() {
const user = await getUser();
const posts = await getPosts(); // 必须等 getUser 完成
return <Feed user={user} posts={posts} />;
}
// ✅ 并行发起,延迟 await
async function Page() {
const userPromise = getUser();
const postsPromise = getPosts();
const [user, posts] = await Promise.all([userPromise, postsPromise]);
return <Feed user={user} posts={posts} />;
}
1.2 用 Promise.all() 并行化独立操作
当多个异步操作互不依赖时,始终使用 Promise.all() 并行执行。
// ❌ 串行:总时间 = T1 + T2 + T3
const user = await fetchUser(id);
const orders = await fetchOrders(id);
const recommendations = await fetchRecommendations(id);
// ✅ 并行:总时间 = max(T1, T2, T3)
const [user, orders, recommendations] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchRecommendations(id),
]);
1.3 策略性放置 Suspense 边界
用 Suspense 拆分页面,让快的部分先展示,慢的部分异步加载。
// ✅ 外壳立即可见,数据部分逐步流式加载
export default function DashboardPage() {
return (
<DashboardShell>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</DashboardShell>
);
}
1.4 API Route 中避免瀑布链
在 API Route / Server Action 中同样适用——先发起所有请求,再 await。
1.5 有依赖关系时最大化并行度
当操作之间存在部分依赖时,用依赖图分析哪些可以并行。
// ✅ user 和 config 并行;posts 依赖 user,但 config 不阻塞它
const [user, config] = await Promise.all([getUser(), getConfig()]);
const posts = await getPosts(user.id);
二、Bundle 体积优化(CRITICAL)
2.1 避免 Barrel File 导入
Barrel file(index.ts 聚合导出)会导致 tree-shaking 失效,一个图标可能拉入整个图标库。
// ❌ 拉入全部图标(200KB+)
import { Check } from "lucide-react";
// ✅ 直接从源文件导入
import Check from "lucide-react/dist/esm/icons/check";
适用于所有 icon 库、UI 组件库、工具库。
2.2 动态导入重型组件
对大型组件(编辑器、图表、地图等)使用 next/dynamic 或 React.lazy。
// ✅ Monaco Editor (~300KB) 仅在需要时加载
import dynamic from "next/dynamic";
const CodeEditor = dynamic(() => import("@/components/CodeEditor"), {
loading: () => <EditorSkeleton />,
ssr: false,
});
2.3 延迟加载非关键第三方库
分析脚本、错误追踪等不应阻塞首屏。
// ✅ hydration 完成后再加载分析脚本
useEffect(() => {
import("@analytics/core").then((mod) => mod.init());
}, []);
2.4 条件加载模块
仅在功能激活时才加载对应的模块/数据。
2.5 基于用户意图预加载
在 hover/focus 时预加载即将使用的组件。
// ✅ hover 时预加载,点击时立即可用
<button
=> import("@/components/SettingsPanel")}
=> setShowSettings(true)}
>
设置
</button>
三、服务端性能(HIGH)
3.1 Server Action 必须验证身份
Server Action 是公开端点,必须像 API Route 一样验证认证和授权。
"use server";
export async function deletePost(postId: string) {
const session = await auth(); // ✅ 始终验证
if (!session) throw new Error("Unauthorized");
if (!await canDelete(session.user.id, postId)) throw new Error("Forbidden");
await db.post.delete({ where: { id: postId } });
}
3.2 最小化 RSC 边界的序列化
Server Component 传给 Client Component 的每个 prop 都会被序列化进 HTML。只传客户端需要的字段。
// ❌ 传了整个 user 对象(包含敏感字段)
<UserCard user={user} />
// ✅ 只传需要的字段
<UserCard name={user.name} avatar={user.avatar} />
3.3 用 React.cache() 做请求内去重
同一次请求中多个组件调用同一个数据函数时,用 React.cache() 避免重复执行。
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// 同一请求中多次调用 getUser(id) 只执行一次
3.4 跨请求使用 LRU 缓存
React.cache() 仅在单次请求内生效。对跨请求共享的数据,使用 LRU 缓存。
import { LRUCache } from "lru-cache";
const cache = new LRUCache<string, any>({ max: 500, ttl: 1000 * 60 * 5 });
export async function getConfig() {
const cached = cache.get("config");
if (cached) return cached;
const config = await db.config.findFirst();
cache.set("config", config);
return config;
}
3.5 静态 I/O 提升到模块级别
字体文件、Logo、静态配置等不要每次请求都读取。
// ✅ 模块加载时读取一次
const logoBuffer = await fs.readFile("./public/logo.png");
export function Logo() {
return <img src={`data:image/png;base64,${logoBuffer.toString("base64")}`} />;
}
3.6 用 after() 做非阻塞操作
日志、埋点等不影响响应的操作用 after() 推迟到响应发送之后。
import { after } from "next/server";
export async function POST(request: Request) {
const result = await processOrder(request);
after(async () => {
await logAnalytics({ event: "order_created", orderId: result.id });
});
return Response.json(result);
}
3.7 并行数据获取 + 组件组合
利用 Server Component 的组合特性,让独立的数据获取自动并行。
3.8 避免重复序列化
不要同时传原始数组和衍生状态,让客户端自行计算。
四、客户端数据获取(MEDIUM-HIGH)
4.1 用 SWR/TanStack Query 自动去重
多个组件实例订阅同一数据时自动共享请求。
// ✅ 多个组件调用同一 key,只发一次请求
function useUser(id: string) {
return useSWR(`/api/users/${id}`, fetcher);
}
4.2 全局事件监听器去重
用 useSyncExternalStore 或 useSWRSubscription 共享 WebSocket/事件源。
4.3 使用 Passive Event Listener
滚动、触摸事件加 { passive: true },消除浏览器滚动延迟。
// ✅ 不阻塞滚动
element.addEventListener("touchstart", handler, { passive: true });
4.4 localStorage 加版本号并最小化存储
// ✅ 带版本前缀 + 只存必要字段 + try-catch 包裹
const STORAGE_KEY = "app_prefs_v2";
function savePrefs(prefs: UserPrefs) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
theme: prefs.theme,
lang: prefs.lang,
}));
} catch (e) {
// 存储已满或隐私模式
}
}
五、重渲染优化(MEDIUM)
5.1 渲染期间计算派生状态
不要把可以计算的值存进 state——直接在渲染中计算。
// ❌ 多余的 state + effect
const [items, setItems] = useState(data);
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter((i) => i.active));
}, [items]);
// ✅ 渲染期间直接计算
const [items, setItems] = useState(data);
const filteredItems = items.filter((i) => i.active);
5.2 不要在组件内定义组件
内联组件每次父组件渲染都会导致完全卸载+重新挂载。
// ❌ 每次渲染都创建新组件
function Parent() {
function Child() { return <div>child</div>; } // 别这样做
return <Child />;
}
// ✅ 提取到外部,用 props 传递数据
function Child({ data }: { data: string }) {
return <div>{data}</div>;
}
function Parent() {
return <Child data="hello" />;
}
5.3 用 useRef 存储不触发渲染的频繁变化值
鼠标位置、滚动进度、计时器 ID 等不需要触发 UI 更新的值,用 ref 而非 state。
5.4 用 useMemo/React.memo 控制子组件渲染
但注意:简单表达式不要包 useMemo。useMemo 本身有开销,布尔值/数字/字符串的简单计算不值得 memo。
// ❌ 过度 memo
const isActive = useMemo(() => status === "active", [status]);
// ✅ 直接计算
const isActive = status === "active";
5.5 用函数式 setState 避免闭包陷阱
// ❌ 可能使用过期的 count
setCount(count + 1);
// ✅ 始终基于最新值
setCount((prev) => prev + 1);
5.6 useState 惰性初始化
传函数给 useState,仅在首次渲染时执行。
// ❌ 每次渲染都执行 expensive()
const [data, setData] = useState(expensiveComputation());
// ✅ 只在挂载时执行一次
const [data, setData] = useState(() => expensiveComputation());
5.7 提取非原始值默认值为常量
// ❌ 每次渲染新建对象,memo 失效
function List({ config = { pageSize: 10 } }) { ... }
// ✅ 稳定引用
const DEFAULT_CONFIG = { pageSize: 10 };
function List({ config = DEFAULT_CONFIG }) { ... }
5.8 缩窄 Effect 依赖
订阅派生的布尔值而非连续变化的值。
// ❌ value 每次变化都触发 effect
useEffect(() => { ... }, [value]);
// ✅ 只在 isAboveThreshold 变化时触发
const isAboveThreshold = value > 100;
useEffect(() => { ... }, [isAboveThreshold]);
5.9 把交互逻辑放进事件处理器
用户行为 -> 事件处理器,不是 state + effect 间接表达。
5.10 延迟读取动态状态
如果只在回调中读取 searchParams/localStorage,不要在渲染层订阅它们。
5.11 用 startTransition 标记非紧急更新
// ✅ 搜索输入立即响应,结果列表延迟更新
function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
startTransition(() => {
setResults(searchItems(e.target.value));
});
}
return (
<>
<input value={query} />
<ResultList results={results} />
</>
);
}
5.12 提取为 Memo 组件实现提前返回
5.13 Effect 内不该做的事 -> 移到事件处理器
六、渲染性能(MEDIUM)
6.1 用 CSS content-visibility 优化长列表
/* ✅ 浏览器跳过屏幕外内容的渲染 */
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
6.2 静态 JSX 提升到组件外部
不会变化的 JSX 片段提取为模块级常量,避免每次渲染重新创建。
// ✅ 只创建一次
const EMPTY_STATE = <div className="empty">暂无数据</div>;
function List({ items }: { items: Item[] }) {
if (items.length === 0) return EMPTY_STATE;
return <ul>{items.map(...)}</ul>;
}
6.3 防止 Hydration 闪烁
依赖 localStorage/cookie 的 UI(如暗色模式)用同步脚本在 React hydrate 之前设置。
<!-- ✅ 在 <head> 中同步执行,hydration 前完成 -->
<script>
const theme = localStorage.getItem("theme") || "light";
document.documentElement.dataset.theme = theme;
</script>
6.4 使用 Activity 组件(React 19+)
频繁切换显示/隐藏的重型组件,用 <Activity> 保留 state 和 DOM。
// ✅ 切换时保留状态,不重新挂载
import { Activity } from "react";
<Activity mode={activeTab === "editor" ? "visible" : "hidden"}>
<HeavyEditor />
</Activity>
6.5 SVG 动画包裹 div
直接在 SVG 元素上做 CSS 动画会跳过 GPU 加速。包一层 div。
6.6 优化 SVG 精度
用 SVGO 将坐标精度降到 1 位小数,减少 SVG 体积 20-40%。
6.7 script 标签始终加 defer/async
6.8 用 useTransition 替代手动 loading state
// ❌ 手动管理 loading
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
setIsLoading(true);
await doSomething();
setIsLoading(false);
}
// ✅ 内置 isPending
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(async () => {
await doSomething();
});
}
6.9 用显式三元替代 && 渲染
// ❌ count 为 0 时渲染 "0" 而非 null
{count && <Badge count={count} />}
// ✅ 明确处理 falsy 值
{count > 0 ? <Badge count={count} /> : null}
6.10 使用 React DOM 资源提示
import { prefetchDNS, preconnect, preload } from "react-dom";
// ✅ 预连接关键第三方域
preconnect("https://api.example.com");
preload("/fonts/inter.woff2", { as: "font", type: "font/woff2" });
6.11 suppressHydrationWarning 用于已知差异
日期、随机 ID 等已知的 SSR/客户端差异,使用 suppressHydrationWarning。
七、JavaScript 性能(LOW-MEDIUM)
7.1 避免布局抖动
批量写入样式,再读取布局属性。交错读写会强制同步 reflow。
// ❌ 每次循环都触发 reflow
elements.forEach((el) => {
el.style.width = `${container.offsetWidth}px`; // 读 -> 写 -> 读 -> 写
});
// ✅ 先读,再批量写
const width = container.offsetWidth;
elements.forEach((el) => {
el.style.width = `${width}px`;
});
7.2 用 Map/Set 替代数组查找
O(1) vs O(n) 的差距在大数据集下指数级放大。
// ❌ O(n) 查找
const user = users.find((u) => u.id === targetId);
// ✅ O(1) 查找
const userMap = new Map(users.map((u) => [u.id, u]));
const user = userMap.get(targetId);
7.3 循环中缓存属性访问
7.4 缓存重复函数调用结果
7.5 缓存 Storage API 调用
localStorage/sessionStorage 是同步阻塞的,用内存缓存避免重复读取。
7.6 合并多次数组迭代
// ❌ 两次迭代
const active = users.filter((u) => u.active).map((u) => u.name);
// ✅ 一次迭代
const active = users.reduce<string[]>((acc, u) => {
if (u.active) acc.push(u.name);
return acc;
}, []);
// ✅ 或用 flatMap
const active = users.flatMap((u) => (u.active ? [u.name] : []));
7.7 先检查长度再比较数组
7.8 函数提前返回
确定结果后立即 return,不要走完所有分支。
7.9 正则表达式提升到模块级
// ❌ 每次渲染都编译正则
function validate(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// ✅ 模块级编译一次
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validate(email: string) {
return EMAIL_RE.test(email);
}
7.10 用 flatMap 替代 filter + map
7.11 求最值用循环而非排序
O(n) vs O(n log n)。
7.12 用 toSorted() 替代 sort()
toSorted() 返回新数组,不会 mutate 原数组,避免 React state 变异 bug。
7.13 提前退出循环
八、高级模式(LOW)
8.1 模块级初始化而非 useEffect
// ❌ StrictMode 下执行两次,组件重新挂载时再次执行
useEffect(() => {
analytics.init();
}, []);
// ✅ 模块级,只执行一次
let initialized = false;
function initOnce() {
if (initialized) return;
initialized = true;
analytics.init();
}
initOnce();
8.2 用 Ref 存储事件处理器
保持稳定的订阅引用,避免回调变化时反复重新订阅。
const handlerRef = useRef(onMessage);
handlerRef.current = onMessage; // 每次渲染更新引用
useEffect(() => {
const handler = (e: MessageEvent) => handlerRef.current(e.data);
ws.addEventListener("message", handler);
return () => ws.removeEventListener("message", handler);
}, []); // 依赖数组为空——永不重新订阅
8.3 useEffectEvent(React 19+)
在回调中访问最新值,而不触发 Effect 重新执行。
// ✅ React 19+ 实验 API
const number) => {
console.log(latestConfig, time); // 始终拿到最新 config
});
useEffect(() => {
const id = setInterval(() => onTick(Date.now()), 1000);
return () => clearInterval(id);
}, []); // 不需要依赖 config
九、React 19+ 新特性速查
React 19 引入了多个范式级变更,以下是关键新 API:
9.1 use() Hook
在组件中直接读取 Promise 和 Context,替代部分 useEffect + useState 模式。
// ✅ React 19: 直接在组件中 unwrap Promise
import { use } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // 配合 Suspense 使用
return <div>{user.name}</div>;
}
9.2 Actions(useActionState + useFormStatus)
表单提交的一等公民支持。
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "提交中..." : "提交"}</button>;
}
function LoginForm() {
const [state, formAction] = useActionState(loginAction, { error: null });
return (
<form action={formAction}>
<input name="email" type="email" required />
{state.error && <p className="error">{state.error}</p>}
<SubmitButton />
</form>
);
}
9.3 useOptimistic
乐观更新,提升交互感知速度。
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMsg: string) => [...state, { text: newMsg, sending: true }]
);
async function sendMessage(text: string) {
addOptimistic(text); // 立即显示
await submitMessage(text); // 后台提交
}
9.4 ref 作为 prop 直接传递
React 19 中函数组件直接接收 ref 作为 prop,不再需要 forwardRef。
// ✅ React 19: 直接传递
function Input({ ref, ...props }: { ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
9.5 文档元数据原生支持
// ✅ React 19: 组件内直接声明 <title>、<meta>、<link>
function BlogPost({ post }: { post: Post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
十、Server Components 与 Next.js 15+ 实战
10.1 组件划分原则
Server Component(默认) Client Component('use client')
───────────────────────── ─────────────────────────────
数据获取 事件处理(onClick 等)
敏感逻辑/密钥访问 useState / useEffect / useRef
数据库直连 浏览器 API(localStorage 等)
静态渲染 第三方仅客户端库
大型依赖(不发送到客户端) 实时交互组件
10.2 组合模式:Server 包 Client
// ✅ Server Component 获取数据,Client Component 处理交互
// app/dashboard/page.tsx (Server Component)
export default async function DashboardPage() {
const stats = await getStats(); // 服务端直连数据库
return (
<div>
<h1>仪表盘</h1>
<StatsDisplay initialData={stats} /> {/* Client Component */}
</div>
);
}
// components/StatsDisplay.tsx
"use client";
export function StatsDisplay({ initialData }: { initialData: Stats }) {
const [data, setData] = useState(initialData);
// 客户端交互逻辑...
}
10.3 Next.js 15+ 缓存策略
// 静态缓存(构建时)
fetch(url, { cache: "force-cache" });
// 按时间重新验证
fetch(url, { next: { revalidate: 3600 } });
// 动态不缓存
fetch(url, { cache: "no-store" });
// 按需重新验证
import { revalidateTag, revalidatePath } from "next/cache";
revalidateTag("posts");
revalidatePath("/blog");
10.4 Partial Prerendering(PPR)
Next.js 15+ 的混合渲染模式,静态外壳 + 动态内容流式加载。
// next.config.ts
export default { experimental: { ppr: true } };
// 页面中:静态部分预渲染,Suspense 内动态流式加载
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<ProductLayout>
{/* 这部分是静态的,预渲染到 HTML */}
<ProductHeader id={params.id} />
<Suspense fallback={<PriceSkeleton />}>
{/* 这部分是动态的,流式加载 */}
<DynamicPrice id={params.id} />
</Suspense>
</ProductLayout>
);
}
10.5 Server Actions 最佳实践
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
});
export async function createPost(formData: FormData) {
// 1. 验证身份
const session = await auth();
if (!session) throw new Error("Unauthorized");
// 2. 验证输入
const parsed = CreatePostSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
});
if (!parsed.success) return { error: parsed.error.flatten() };
// 3. 执行操作
await db.post.create({ data: { ...parsed.data, authorId: session.user.id } });
// 4. 更新缓存
revalidatePath("/posts");
return { success: true };
}
能力边界声明
当用户的需求涉及以下方面时,简要提醒一次,然后聚焦回 React 代码最佳实践:
| 需求 | 建议 |
|---|---|
| UI 美化 / 视觉设计 / 页面太丑 | 推荐使用 frontend-design 技能(/frontend-design),专精于界面视觉优化 |
| CSS 动效设计 / 微交互 | 推荐使用 frontend-design 技能处理视觉层面,本技能聚焦于 React 代码模式 |
提醒方式示例:"UI 美化推荐试试 /frontend-design 技能,它专门处理视觉设计。我这边聚焦帮你把 React 代码写对写好。"
只在首次遇到时提醒一次,不要反复提及。