- name
- react-best-practices
- description
- |
## 适用时机
在以下场景中使用本指南:
- 编写新的 React 组件或页面
- 实现数据获取逻辑
- 审查代码性能问题
- 重构已有代码
- 优化 bundle 大小和加载时间
- 迁移到 React 19+ / Next.js 15+ / Server Components
---
## 一、消除请求瀑布(CRITICAL)
请求瀑布是性能的头号杀手。多个异步操作串行执行时,总延迟 = 所有操作之和。
### 1.1 延迟 await 到真正需要的位置
```typescript
// ❌ 阻塞整个函数
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()` 并行执行。
```typescript
// ❌ 串行:总时间 = 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 拆分页面,让快的部分先展示,慢的部分异步加载。
```tsx
// ✅ 外壳立即可见,数据部分逐步流式加载
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 有依赖关系时最大化并行度
当操作之间存在部分依赖时,用依赖图分析哪些可以并行。
```typescript
// ✅ 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 失效,一个图标可能拉入整个图标库。
```typescript
// ❌ 拉入全部图标(200KB+)
import { Check } from "lucide-react";
// ✅ 直接从源文件导入
import Check from "lucide-react/dist/esm/icons/check";
```
适用于所有 icon 库、UI 组件库、工具库。
### 2.2 动态导入重型组件
对大型组件(编辑器、图表、地图等)使用 `next/dynamic` 或 `React.lazy`。
```typescript
// ✅ Monaco Editor (~300KB) 仅在需要时加载
import dynamic from "next/dynamic";
const CodeEditor = dynamic(() => import("@/components/CodeEditor"), {
loading: () => <EditorSkeleton />,
ssr: false,
});
```
### 2.3 延迟加载非关键第三方库
分析脚本、错误追踪等不应阻塞首屏。
```typescript
// ✅ hydration 完成后再加载分析脚本
useEffect(() => {
import("@analytics/core").then((mod) => mod.init());
}, []);
```
### 2.4 条件加载模块
仅在功能激活时才加载对应的模块/数据。
### 2.5 基于用户意图预加载
在 hover/focus 时预加载即将使用的组件。
```tsx
// ✅ hover 时预加载,点击时立即可用
<button
onMouseEnter={() => import("@/components/SettingsPanel")}
onClick={() => setShowSettings(true)}
>
设置
</button>
```
---
## 三、服务端性能(HIGH)
### 3.1 Server Action 必须验证身份
Server Action 是公开端点,必须像 API Route 一样验证认证和授权。
```typescript
"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。只传客户端需要的字段。
```typescript
// ❌ 传了整个 user 对象(包含敏感字段)
<UserCard user={user} />
// ✅ 只传需要的字段
<UserCard name={user.name} avatar={user.avatar} />
```
### 3.3 用 React.cache() 做请求内去重
同一次请求中多个组件调用同一个数据函数时,用 `React.cache()` 避免重复执行。
```typescript
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 缓存。
```typescript
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、静态配置等不要每次请求都读取。
```typescript
// ✅ 模块加载时读取一次
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()` 推迟到响应发送之后。
```typescript
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 自动去重
多个组件实例订阅同一数据时自动共享请求。
```typescript
// ✅ 多个组件调用同一 key,只发一次请求
function useUser(id: string) {
return useSWR(`/api/users/${id}`, fetcher);
}
```
### 4.2 全局事件监听器去重
用 `useSyncExternalStore` 或 `useSWRSubscription` 共享 WebSocket/事件源。
### 4.3 使用 Passive Event Listener
滚动、触摸事件加 `{ passive: true }`,消除浏览器滚动延迟。
```typescript
// ✅ 不阻塞滚动
element.addEventListener("touchstart", handler, { passive: true });
```
### 4.4 localStorage 加版本号并最小化存储
```typescript
// ✅ 带版本前缀 + 只存必要字段 + 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——直接在渲染中计算。
```typescript
// ❌ 多余的 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 不要在组件内定义组件
内联组件每次父组件渲染都会导致完全卸载+重新挂载。
```typescript
// ❌ 每次渲染都创建新组件
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。
```typescript
// ❌ 过度 memo
const isActive = useMemo(() => status === "active", [status]);
// ✅ 直接计算
const isActive = status === "active";
```
### 5.5 用函数式 setState 避免闭包陷阱
```typescript
// ❌ 可能使用过期的 count
setCount(count + 1);
// ✅ 始终基于最新值
setCount((prev) => prev + 1);
```
### 5.6 useState 惰性初始化
传函数给 `useState`,仅在首次渲染时执行。
```typescript
// ❌ 每次渲染都执行 expensive()
const [data, setData] = useState(expensiveComputation());
// ✅ 只在挂载时执行一次
const [data, setData] = useState(() => expensiveComputation());
```
### 5.7 提取非原始值默认值为常量
```typescript
// ❌ 每次渲染新建对象,memo 失效
function List({ config = { pageSize: 10 } }) { ... }
// ✅ 稳定引用
const DEFAULT_CONFIG = { pageSize: 10 };
function List({ config = DEFAULT_CONFIG }) { ... }
```
### 5.8 缩窄 Effect 依赖
订阅派生的布尔值而非连续变化的值。
```typescript
// ❌ value 每次变化都触发 effect
useEffect(() => { ... }, [value]);
// ✅ 只在 isAboveThreshold 变化时触发
const isAboveThreshold = value > 100;
useEffect(() => { ... }, [isAboveThreshold]);
```
### 5.9 把交互逻辑放进事件处理器
用户行为 -> 事件处理器,不是 state + effect 间接表达。
### 5.10 延迟读取动态状态
如果只在回调中读取 searchParams/localStorage,不要在渲染层订阅它们。
### 5.11 用 startTransition 标记非紧急更新
```typescript
// ✅ 搜索输入立即响应,结果列表延迟更新
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} onChange={handleChange} />
<ResultList results={results} />
</>
);
}
```
### 5.12 提取为 Memo 组件实现提前返回
### 5.13 Effect 内不该做的事 -> 移到事件处理器
---
## 六、渲染性能(MEDIUM)
### 6.1 用 CSS content-visibility 优化长列表
```css
/* ✅ 浏览器跳过屏幕外内容的渲染 */
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
### 6.2 静态 JSX 提升到组件外部
不会变化的 JSX 片段提取为模块级常量,避免每次渲染重新创建。
```typescript
// ✅ 只创建一次
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 之前设置。
```html
<!-- ✅ 在 <head> 中同步执行,hydration 前完成 -->
<script>
const theme = localStorage.getItem("theme") || "light";
document.documentElement.dataset.theme = theme;
</script>
```
### 6.4 使用 Activity 组件(React 19+)
频繁切换显示/隐藏的重型组件,用 `<Activity>` 保留 state 和 DOM。
```tsx
// ✅ 切换时保留状态,不重新挂载
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%。
GitHubで見る