vault backup: 2026-04-29 23:36:32

This commit is contained in:
2026-04-29 23:36:32 +08:00
parent 7d41fbcd6a
commit c695463715
9 changed files with 1823 additions and 297 deletions
+205 -52
View File
@@ -1,5 +1,5 @@
---
tags: [React, Component Communication, Props, Context, Frontend]
tags: [React, Component Communication, Props, Context, State Lifting, Frontend]
create time: 2026-04-29 22:10
---
@@ -7,7 +7,12 @@ create time: 2026-04-29 22:10
## 概述
React 中父子组件之间的数据流动有多种方式。理解每种模式的适用场景和性能影响,能够避免过度设计或通信瓶颈。本文档从简单到复杂,梳理完整的通信方案谱系。
React 的核心理念是 **"单向数据流"** ——数据从父组件流向子组件,事件从子组件冒泡回父组件。但在真实项目中,组件层级可以很深、结构可以很散,跨组件共享状态成为日常需求。
本文档梳理 React 中所有主流的组件通信方式,从最基础的 Props 到状态管理库,帮助你根据实际场景选择最合适方案。
> [!question] 思考题
> 假设你有一个 6 层深的组件树,最顶层需要把 `theme` 传给第 6 层的按钮组件。你会怎么做?逐层透传 Props 似乎笨拙,但引入 Context 又可能引发不必要的重渲染——你怎么权衡?
## 通信方向全景图
@@ -16,14 +21,14 @@ graph TD
A["父 → 子"] --> B["Props(最基础)"]
A --> C["Context Provider(跨层级)"]
A --> D["状态管理库(全局共享)"]
E["子 → 父"] --> F["回调 Prop(事件驱动)"]
E --> G["refs(imperative)"]
E --> G["Refs(imperative)"]
H["兄弟组件"] --> I["状态提升到共同父组件"]
H --> J["通过共同祖先通信"]
H --> K["状态管理库 / 发布订阅"]
style B fill:#4FC08D,color:#fff
style C fill:#F5A87D,color:#000
style F fill:#61DAFB,color:#000
@@ -36,30 +41,37 @@ graph TD
```tsx
function App() {
const user = getUser();
return <Header user={user} />; // Header 不需要 user,但深层的 Avatar 需要!
return <Header user={user} />;
// Header 不需要 user,但深层的 Avatar 需要!
}
function Header({ user }) {
return (
<nav>
<Logo /> {/* ❌ 无需 user,却接收了 */}
<Avatar user={user} /> {/* ✅ 需要 */}
<Logo />
{/* ❌ 无需 user,却被迫接收 */}
<Avatar user={user} />
</nav>
);
}
```
> [!tip] 何时该容忍 Props Drill?
> 传递 2-3 层 Props 是完全合理的,甚至值得鼓励——它让数据流向清晰可见。只有超过 4 层时才考虑替代方案。
### 解决方案对比
| 方法 | 适合场景 | 额外依赖 |
|------|----------|---------|
| **Context** | 少量深层传递(主题、用户信息) | 无 |
| **自定义 Hook + Props 重组织** | 中等深度(3-4 层) | 无 |
| **状态管理库** | 频繁变化、多组件共享 | Zustand/Redux |
| **Render Prop / HOC** | 复用逻辑而非传值 | 无 |
| **Context** | 少量深层传递(主题、用户信息、语言设置) | 无 |
| **自定义 Hook + Props 重组织** | 中等深度(3-4 层),只需中间层不暴露 | 无 |
| **状态管理库** | 频繁变化、多组件共享 | Zustand / Redux Toolkit |
| **Render Props / HOC** | 复用逻辑而非单纯传值 | 无 |
## Callback Prop(子 → 父)
这是 React 中最基础也最重要的反向通信方式:**父组件传递一个函数作为 Prop,子组件在适当时机调用它**。
```tsx
interface FormProps {
onSubmit: (data: FormValues) => Promise<void>;
@@ -69,9 +81,9 @@ function LoginForm({ onSubmit }: FormProps) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const data = collectFormData();
onSubmit(data); // 通知父组件
onSubmit(data); // ✅ 通知父组件
};
return <form onSubmit={handleSubmit}><Button type="submit">登录</Button></form>;
}
@@ -81,75 +93,203 @@ function App() {
await api.login(data);
navigate("/dashboard");
};
return <LoginForm onSubmit={handleLogin} />;
}
```
> [!note] 为什么不用 props.children 做通信?
> children 是 UI 内容插槽,不是通信机制。通信用 callback prop 保持关注点分离。
> [!note] 为什么不用 children 做通信?
> children 是 UI 内容插槽,不是通信机制。回调 Prop 保持关注点分离——子组件只关心"什么时候触发",父组件决定"触发后做什么"。
> [!important] 性能提示:使用 useCallback
> 如果子组件用 `React.memo` 包裹,每次父组件重新渲染都会创建新的函数引用,导致子组件无效重渲染。用 `useCallback` 缓存回调函数:
> ```tsx
> const handleLogin = useCallback(async (data: FormValues) => { ... }, []);
> ```
## Context 跨层级通信
当 Props 穿透超过 3-4 层时,Context 是最轻量的替代方案。**它的本质是一个"隐式 Prop"——Provider 上方的任意后代都可以直接消费,无需经过中间组件。**
```tsx
const UserContext = createContext<User | null>(null);
function UserProfile({ children }: { children: React.ReactNode }) {
function UserProfileProvider({ children }: { children: React.ReactNode }) {
const user = useDatabaseUser(userId);
return (
<UserContext.Provider value={user}>
{/* Avatar、Settings、OrderHistory 任意深度都可消费 */}
{children}
{children} {/* Avatar、Settings 任意深度都可消费 */}
</UserContext.Provider>
);
}
function OrderHistory() {
const user = useContext(UserContext); // 直接获取,无需中间层透传
return <div>{user?.orders?.map(...)}</div>;
const user = useContext(UserContext); // ✅ 直接获取,跳过中间层
return <div>{user?.orders?.map(order => <OrderItem key={order.id} order={order} />)}</div>;
}
```
> [!warning] Context 的性能陷阱
> - Provider value 对象每次渲染都是新引用 → 所有消费者重渲染
> - 解决:拆分 Context、或使用 `useReducer` 返回稳定的 `{state, dispatch}` 对象
### Context 性能陷阱与最佳实践
```
⚠️ 核心问题:
Context value 改变 → Provider 下所有消费者重渲染,无论是否真的使用了这个值
```
**陷阱一:value 对象每次都是新引用**
```tsx
// ❌ 每次渲染都创建新对象,所有消费者必重渲染
<UserContext.Provider value={{ user, theme }}>
{children}
</UserContext.Provider>
```
**陷阱二:单个大 Context 包含多个独立状态**
```tsx
// ❌ settings 变了,即使用户信息模块的组件也会重渲染
<UserAndSettingsContext.Provider value={{ user, settings, locale }}>
{children}
</UserAndSettingsContext.Provider>
```
**最佳实践:拆分成小 Context + useReducer 稳定引用**
```tsx
// ✅ 拆分 Context,粒度更细
const UserContext = createContext<User | null>(null);
const SettingsContext = createContext<Settings>({ theme: "light" });
// ✅ 用 useReducer 返回稳定的 { state, dispatch } 对象
function SettingsProvider({ children }: { children: React.ReactNode }) {
const [settings, dispatch] = useReducer(settingsReducer, initialSettings);
return (
<SettingsContext.Provider value={{ settings, dispatch }}>
{children}
</SettingsContext.Provider>
);
}
// 消费者只取需要的 slice
function ThemeToggle() {
const { settings, dispatch } = useContext(SettingsContext);
return (
<button onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
{settings.theme === "light" ? "☀️" : "🌙"}
</button>
);
}
```
## 兄弟组件通信
两个没有祖先后代关系的组件如何共享数据?答案是:**将状态提升到它们的共同父组件中**。
```tsx
type ContactId = string;
function ChatApp() {
const [contacts, setContacts] = useState<Contact[]>([]);
const [selectedId, setSelectedId] = useState<ContactId | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const selectedContact = useMemo(
() => contacts.find(c => c.id === selectedId),
[contacts, selectedId]
);
return (
<div className="chat-layout">
{/* 左侧:联系人列表 */}
<ContactList
contacts={contacts}
selectedId={selectedId}
onSelect={setSelectedId}
/>
{/* 右侧:聊天窗口 */}
<ChatWindow
contact={selectedContact}
messages={messages.filter(m => m.contactId === selectedId)}
onSend={(text) => setMessages(prev => [...prev, { text, contactId: selectedId!, timestamp: Date.now() }])}
/>
</div>
);
}
```
> [!diagram] 状态提升流程
> ```mermaid
> graph LR
> A[ContactList] -- "onSelect(id)" --> P[ChatApp - State]
> P -- "contact" --> B[ChatWindow]
> P -- "onSend(text)" --> P
> B -- "sendMessage(text)" --> P
> style P fill:#4FC08D,color:#fff
> ```
> [!tip] 何时应该使用状态提升?
> 当兄弟组件数量少(≤ 3 个)、关系固定、状态不频繁变化时,状态提升比引入状态管理库更简单、更可维护。
## 状态管理库方案
当状态需要在整个应用中流转、涉及复杂操作逻辑或多条副作用时,Zustand / Redux Toolkit 等库提供了集中式状态容器。
```tsx
// Zustand 方案:任意两个组件共享 state,无需祖先后代关系
// Zustand:极简 API,按 slice 订阅避免全量重渲染
import { create } from "zustand";
const useCartStore = create((set) => ({
interface CartState {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
}
const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
}));
function ProductCard() {
const addItem = useCartStore(s => s.addItem);
function ProductCard({ product }: { product: Product }) {
const addItem = useCartStore(s => s.addItem); // 精确订阅,仅在 addItem 引用变化时重渲染
return <button onClick={() => addItem(product)}>加入购物车</button>;
}
function CartIcon() {
const items = useCartStore(s => s.items);
const items = useCartStore(s => s.items); // 仅 items 变化时重渲染
return <Badge>{items.length}</Badge>;
}
// 两者毫无关联,但共享同一份状态
```
> [!note] Zustand vs Redux Toolkit 选型建议
> - **Zustand**:API 简洁、样板代码极少、支持 immer 中间件,适合中小型项目和个人项目
> - **Redux Toolkit**:生态成熟、DevTools 体验好、middleware 体系完善(thunk/saga),适合大型团队协作项目
> - 详见 [[hhs/REACT/3. 生态工具篇/09-状态管理.md]]
## Ref Imperative API(命令式通信)
大多数情况下 React 推崇声明式通信(Props + Callback),但在需要**直接操控子组件实例行为**时使用 refs。典型场景:触发自定义验证、聚焦输入框、播放动画。
```tsx
const formRef = useRef<FormHandle>(null);
function Parent() {
const handleSubmit = () => {
formRef.current?.validate(); // 命令子组件执行方法
formRef.current?.validate(); // 命令子组件执行
formRef.current?.reset();
};
return <><ChildForm ref={formRef} /><button onClick={handleSubmit}>提交</button></>;
return (
<>
<ChildForm ref={formRef} />
<button onClick={handleSubmit}>提交</button>
</>
);
}
const ChildForm = forwardRef<FormHandle>((props, ref) => {
@@ -157,31 +297,44 @@ const ChildForm = forwardRef<FormHandle>((props, ref) => {
validate: () => validator.validate(),
reset: () => setFields(initialValues),
}));
return <form>...</form>;
});
```
> [!warning] 谨慎使用 Imperative Refs
> Imperative API 打破了 React 的声明式范式。能用 declarative(状态驱动 UI)解决的问题,永远不要上 imperative(直接操控 DOM)。过度使用会导致:难以测试、时序 bug、调试困难。
## 决策树
```mermaid
graph TD
A["需要通信?"] -->|"是"| B["通信范围?"]
B --> C["仅父子两层"]
B --> D["多层级穿透"]
B --> E["跨层级 + 兄弟之间"]
B --> F["需命令式调用子组件方法"]
C --> G["Props + Callback ✅"]
D --> H["Context ✅"]
E --> I["Zustand / Redux ✅"]
F --> J["forwardRef + useImperativeHandle ✅"]
style G fill:#4FC08D,color:#fff
style H fill:#61DAFB,color:#000
style I fill:#F5A87D,color:#000
style J fill:#A0AEC0,color:#000
START["需要组件通信?"] -->|"否"| DONE["无需处理 ✅"]
START -->|"是"| RANGE["通信范围?"]
RANGE --> PARENT_CHILD["仅父子两层<br/>(1-2 级深度)"]
RANGE --> DEEP["多层级穿透<br/>(≥ 3 级深度)"]
RANGE --> SIBLING["兄弟组件 / 无关组件"]
RANGE --> IMPERATIVE["需命令式调用子组件方法"]
PARENT_CHILD --> PROP["Props + Callback ✅<br/>简单、直观、可追踪"]
DEEP --> CTX["Context ✅<br/>配合 useReducer 保证性能"]
SIBLING --> SLIFTING["状态提升到共同父 ✅"]
SIBLING --> STORE["状态管理库(复杂场景)"]
IMPERATIVE --> REF["forwardRef +<br/>useImperativeHandle ✅"]
style PROP fill:#4FC08D,color:#fff
style CTX fill:#F5A87D,color:#000
style SLIFTING fill:#61DAFB,color:#000
style STORE fill:#C084FC,color:#fff
style REF fill:#A0AEC0,color:#000
```
## 关联笔记
- [[hhs/REACT/1. 基础篇/03-组件与 Props.md]] — Props 类型校验、组件拆分原则
- [[hhs/REACT/2. Hooks 篇/05-核心 Hooks.md]] — useContext / useRef 原理
- [[hhs/REACT/2. Hooks 篇/06-性能优化 Hooks.md]] — 配合 Context 的性能优化手段
- [[hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md]] — 高阶组件与 Render Props 进阶
- [[hhs/REACT/3. 生态工具篇/09-状态管理.md]] — Zustand / Redux Toolkit 选型对比
- [[hhs/REACT/5. 工程实践篇/15-性能优化.md]] — React.memo、虚拟列表、Bundle 分析
+193 -53
View File
@@ -7,12 +7,22 @@ create time: 2026-04-29 22:11
## 概述
Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中复用它逻辑的两种主要模式。理解它们的原理、适用场景和局限,有助于阅读遗留代码并理解为什么 Hooks 成为更优解。
> [!question] 思考:如果两个组件需要共享同一套数据获取逻辑,该怎么设计?
Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中**复用状态逻辑**的两种主要模式。理解它们的原理、适用场景和局限,不仅有助于阅读遗留代码,更能帮你深刻理解为什么 Hooks 成为更优解——以及哪些场景下旧模式依然不可替代。
| 维度 | HOC | Render Props | Custom Hook |
|------|-----|-------------|-------------|
| **本质** | 函数包裹组件 | prop 传递渲染回调 | 纯函数抽离逻辑 |
| **侵入性** | 不改动原组件 JSX | 需修改 JSX 调用处 | 零侵入,仅在 hooks 内使用 |
| **性能影响** | 额外 wrapper 层 | 同上 | **无额外组件树开销** |
## HOC(高阶组件)
### 概念
> [!question] 类比理解:如果把组件看作"数据 → UI"的转换器,HOC 就是在输入输出之间插入了一层"调料"。
```mermaid
graph LR
A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"]
@@ -48,67 +58,135 @@ const StyledTitle = withTheme(function Title({ title }: Props) {
}); // ✅ TS 推断 Props 不变
```
### 常见 HOC 模式
> [!note] 核心原理
> HOC 本质上是**闭包 + 组合**。`WithAuth` 作为一个新的函数组件,可以正常使用 Hooks(因为它是组件),而包裹的 `WrappedComponent` 只负责渲染。这种方式**不修改原组件的任何代码**,仅通过 props 传递增强信息。
#### 关键细节:displayName
调试嵌套 HOC 时,React DevTools 会显示一层层无意义的 wrapper 名。可以用 `displayName` 让调试更清晰:
```tsx
// 1. 日志/HUD
export function withAuth<P>(Wrapped: React.ComponentType<P>) {
const WithAuth: React.FC<P> = (props) => {
// ... 认证逻辑
return <Wrapped {...props} />;
};
WithAuth.displayName = `withAuth(${Wrapped.name || 'Component'})`;
return WithAuth;
}
// DevTools 中显示:withAuth(Dashboard) → 一目了然
```
### 常见 HOC 模式
#### 1. 日志 / HUD
```tsx
// 通用:为组件自动记录生命周期事件
function withLogger<P extends object>(Comp: React.FC<P>): React.FC<P> {
return function LoggedComponent(props: P) {
useEffect(() => console.log(`${Comp.name} mounted`), []);
useEffect(() => console.log(`[Mount] ${Comp.name || 'Anonymous'}`), []);
return <Comp {...props} />;
};
}
```
> [!tip] 解释
> 不传任何额外 prop,只"旁路"添加副作用(如日志)。这是 HOC 最轻量的用法——原组件完全不知情。
// 2. 加载态封装
#### 2. 加载态封装
```tsx
// 自动管理 loading 状态 + 错误处理
function withLoading<P extends object>(
Comp: React.FC<P>,
fetchData: () => Promise<void>
): React.FC<P> {
return function LoadingWrapper(props: P) {
const [loading, setLoading] = useState(true);
useEffect(() => { fetchData().then(() => setLoading(false)); }, []);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchData().then(() => setLoading(false)).catch(setError);
}, []);
if (error) return <ErrorMessage err={error} />;
return loading ? <Spinner /> : <Comp {...props} />;
};
}
```
> [!note] 解释
> HOC 在**不改动组件渲染逻辑**的前提下,统一接管了 "加载中 / 出错 / 成功" 三种 UI 状态。多个页面复用同一套加载策略,DRY。
// 3. Props 转换(驼峰→kebab)
#### 3. Props 转换
```tsx
// 将 props 做格式变换后注入子组件
function withPropsTransformer<P extends object>(
Comp: React.FC<P>,
transform: (p: P) => Record<string, unknown>
): React.FC {
return function Transformed(props: P) {
const extra = transform(props);
return <Comp {...props as any} {...extra} />;
// ⚠️ transform 返回的 key 可能与原 props 冲突
return <Comp {...(props as any)} {...extra} />;
};
}
```
> [!warning] 注意
> 当 `transform` 返回的 key 和原 props 同名时,后者会覆盖前者。实际项目中建议约定命名空间前缀,例如 `apiData`、`cacheMeta` 等。
### HOC 的局限性
| 问题 | 说明 |
|------|------|
| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps |
| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 |
| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 |
| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) |
| 问题 | 说明 | 解决方案 |
|------|------|---------|
| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps | 手动拷贝:`EnhancedComponent.getInitialProps = Wrapped.getInitialProps` |
| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 | 用 `compose` 函数扁平化,或直接改用 Custom Hook |
| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 | 使用唯一命名空间前缀(如 `_data` → `useData.data`) |
| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) | 见下方兼容 ref 的方案 |
| **隐式依赖** | HOC 内部调用的 hook 不透明,阅读者不知道注入了什么 | 规范命名(`withXxx`),文档说明行为 |
> [!tip] HOC 兼容 ref
> ```tsx
> export function withAuth<P>(Wrapped: React.ComponentType<P>) {
> return React.forwardRef<HTMLDivElement, P>((props, ref) => {
> const { authenticated } = useAuth();
> if (!authenticated) return <Navigate to="/login" />;
> return <Wrapped {...props} ref={ref} />;
> });
> }
> ```
#### 为什么需要 forwardRef?
```tsx
export function withAuth<P>(Wrapped: React.ComponentType<P>) {
return React.forwardRef<HTMLDivElement, P>((props, ref) => {
const { authenticated } = useAuth();
if (!authenticated) return <Navigate to="/login" />;
return <Wrapped {...props} ref={ref} />;
});
}
```
> [!tip] 解释
> 普通函数组件不能接收 `ref`。当 HOC 需要透传 `ref` 给子组件时,必须用 `React.forwardRef` 包裹,否则 React 会抛出 "Function components cannot be given refs" 错误。这增加了额外的样板代码——Hooks 天然解决了这个问题。
#### compose 工具
当多层 HOC 叠加时,`compose` 可以让可读性更好:
```tsx
// before: 从内到外,阅读时要先找最内层
const Enhanced = withAuth(withCache(withPagination(UserList)));
// after: 从左到右,符合阅读习惯
const Enhanced = compose(
withAuth,
withCache,
withPagination,
)(UserList);
```
> [!question] 思考
> compose 真的改善了可读性吗?如果每个 HOC 的作用一目了然,你是否还需要它?
## Render Props
### 核心思想
> [!question] 和 HOC 相比,Render Props 到底"额外"给了什么能力?
通过 prop 传递一个**函数**,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。
与 HOC 的本质区别:HOC 通过**组件嵌套**注入 props;Render Props 通过**函数传参**直接暴露状态。
```tsx
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => React.ReactNode;
@@ -124,6 +202,7 @@ function MouseTracker({ render }: MouseTrackerProps) {
}, []);
// 🎯 关键:render 函数返回什么就渲染什么
// 状态在父组件,UI 由调用方决定
return <div>{render(pos)}</div>;
}
@@ -133,10 +212,15 @@ function MouseTracker({ render }: MouseTrackerProps) {
)} />;
```
### 等价于 children 的情况
> [!note] 解释
> `MouseTracker` 是**纯粹的关注点分离**:它只负责追踪鼠标位置并触发重渲染,完全不关心屏幕上画什么。这种"关注点解耦"是 HOC 难以优雅表达的。
### children 作为 Render Prop
当 render prop 只是把数据传给子内容时,可以用 `children`(本身也是函数)替代——语义更自然。
```tsx
// 当 render prop 只是把数据传给 children 时,可以用 children 替代
// children prop 的类型:接收 data,返回 ReactNode
function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) {
const data = useDatabase();
return <>{children(data)}</>;
@@ -147,42 +231,86 @@ function DataProvider({ children }: { children: (data: Data) => React.ReactNode
</DataProvider>;
```
> [!tip] children vs render
> - **用 children**:渲染逻辑简单、只需要传入数据 → 代码最简洁
> - **用 render prop**:需要多个渲染入口(如 `renderEmpty` / `renderLoading`)、或需要对渲染做参数校验 → 类型约束更精确
### 常见模式:多回调拆分
当渲染逻辑复杂时,可以将一个大 `render` 拆成多个小 prop,降低心智负担:
```tsx
interface CarouselProps {
slides: Slide[];
renderItem: (slide: Slide, index: number) => React.ReactNode;
renderIndicator?: (index: number, active: boolean) => React.ReactNode;
}
function Carousel({ slides, renderItem, renderIndicator }: CarouselProps) {
const [active, setActive] = useState(0);
return (
<div>
{slides.map((s, i) => renderItem(s, i))}
<div className="indicators">
{slides.map((_, i) => renderIndicator?.(i, i === active) ?? (
<span key={i} onClick={() => setActive(i)}
className={i === active ? 'dot-active' : 'dot'} />
))}
</div>
</div>
);
}
```
> [!note] 为什么拆比合好?
> 一个巨大的 `render` 回调会让调用处变得冗长。拆成独立的小 prop(`renderItem`、`renderIndicator`),调用方**按需实现**,未实现的可以省略——这是 React 库设计的经典模式。
## HOC vs Render Props vs Custom Hook
```mermaid
graph TB
A["逻辑复用需求"] --> B["方案对比"]
A["逻辑复用需求"] --> B["三种方案"]
B --> C["HOC"]
B --> D["Render Props"]
B --> E["Custom Hook"]
B --> C["HOC<br/>函数包裹组件"]
B --> D["Render Props<br/>函数传递 prop"]
B --> E["Custom Hook<br/>纯函数抽离逻辑"]
C --> F["⚠️ Wrapper 嵌套深"]
C --> G["⚠️ 静态方法丢失"]
C --> H["✅ 不修改原组件结构"]
C --> C1["⚠️ Wrapper 嵌套深"]
C --> C2["⚠️ 静态方法丢失"]
C --> C3["⚠️ ref 需 forwardRef"]
C --> C4["✅ 不修改原组件 JSX"]
D --> I["⚠️ 回调地狱"]
D --> J["⚠️ Prop 命名冲突风险"]
D --> K["✅ 灵活的 UI 控制"]
D --> D1["⚠️ 回调嵌套过深"]
D --> D2["⚠️ Prop 命名冲突风险"]
D --> D3["✅ UI 控制权完全交给调用方"]
E --> L["✅ 简洁直观"]
E --> M["✅ 可直接操作 state / effect"]
E --> N["✅ 无 wrapper 嵌套"]
E --> O["❌ 只能用于组件内部"]
E --> E1["✅ 扁平可读"]
E --> E2["✅ 直接操作 state / effect"]
E --> E3["✅ 无额外组件树开销"]
E --> E4["❌ 只能在组件内部使用"]
style L fill:#4FC08D,color:#fff
style M fill:#4FC08D,color:#fff
style N fill:#4FC08D,color:#fff
style C4 fill:#4FC08D,color:#fff
style D3 fill:#4FC08D,color:#fff
style E1 fill:#4FC08D,color:#fff
style E2 fill:#4FC08D,color:#fff
style E3 fill:#4FC08D,color:#fff
```
## 为什么 Hooks 取代了它们?
```tsx
// ❌ HOC 方式
const ConnectedUserList = withAuth(withCache(withPagination(UserList)));
// 三层嵌套 → 调试困难、性能不可见、type 推导混乱
Hooks 的核心理念是**把状态逻辑从组件中抽离出来,但保持调用处扁平**。它结合了 HOC 和 Render Props 的优点:
// ✅ Hook 方式
| 维度 | HOC / Render Props | Custom Hook |
|------|-------------------|-------------|
| **嵌套层级** | 多层 wrapper 或回调嵌套 | **零嵌套,直接平铺** |
| **状态共享** | 需要 prop 层层传递,容易混乱 | 每个 hook 管理自己的 state,互不干扰 |
| **TypeScript** | 泛型约束复杂,组合后类型可能丢失 | **完美推断**,不需要额外 type wrangling |
| **调试** | DevTools 多一层 wrapper,栈更混乱 | 与普通函数无异 |
```tsx
// ❌ HOC 方式:三层嵌套 → 调试困难、性能不可见、type 推导混乱
const ConnectedUserList = withAuth(withCache(withPagination(UserList)));
// ✅ Hook 方式:扁平可读,逻辑一目了然
function UserList() {
useAuth(); // 身份验证
const cache = useCache<User[]>(); // 数据缓存
@@ -190,12 +318,24 @@ function UserList() {
return <table>{/* ... */}</table>;
}
// 扁平可读、天然共享 state、TS 完美推断
```
> [!note] HOC 和 Render Props 真的被淘汰了吗?
> - HOC:在需要**包裹**组件但不修改其内部的场景仍有价值(如第三方库封装)
> - Render Props:当父组件需要**完全控制子组件的渲染内容**时仍然有用
> - 但 90%+ 的场景,Custom Hook 是更好的选择
> [!question] 思考
> Hooks 真的完全淘汰了 HOC 和 Render Props 吗?回想一下文档开头的两个核心概念——HOC 通过**包裹**增强组件,Render Props 通过**回调**暴露渲染控制。这两种模式在哪些场景中仍然无法被 Hook 替代?
#### 何时仍应使用旧模式?
| 场景 | 推荐方案 | 原因 |
|------|---------|------|
| 第三方库封装(不接触用户代码) | **HOC** | 库方只操作组件类,无需知道用户内部结构 |
| UI 框架需要提供渲染占位符 | **Render Props** | 父组件需要定义多个渲染回调(如 `renderEmpty`、`renderLoading`) |
| 通用业务逻辑复用 | **Custom Hook** | 首选方案:简洁、可组合、无 DOM 开销 |
## 关联笔记
- [[00.Readme]]
- [[3. Hooks 篇/01-为什么需要 Hooks]]
- [[3. Hooks 篇/02-useEffect 深度解析]]
- [[3. Hooks 篇/08-自定义 Hook 最佳实践]]
- [[4. 进阶篇/07-forwardRef 与 useImperativeHandle]]
- [[4. 进阶篇/11-组件 Composition 模式]]
+106 -15
View File
@@ -7,7 +7,11 @@ create time: 2026-04-29 22:12
## 概述
React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为可中断、可恢复、可优先级调度的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为**可中断、可恢复、可优先级调度**的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
> [!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死?
>
> 答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。
## React 渲染架构演进
@@ -37,6 +41,28 @@ const SettingsPage = lazy(() => import("./pages/SettingsPage"));
</Suspense>
```
### Suspense + SSR(流式渲染)
> [!tip] React 18 的 Streaming SSR
>
> Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。
```tsx
// 服务端组件中嵌套 Suspense boundary
export default async function Page() {
return (
<main>
<Header /> {/* 同步渲染 */}
<Suspense fallback={<SearchSkeleton />}>
<SearchResults /> {/* 等待 fetch 完成后插入 */}
</Suspense>
</main>
);
}
```
服务端输出变成**渐进式 HTML 流**:`Header → SearchSkeleton → SearchResults(loaded)`
### Suspense + Data Fetching(实验性 API)
```tsx
@@ -60,6 +86,10 @@ function Profile() {
## useTransition —— 标记低优先级更新
> [!question] 什么时候该用 Transition?
>
> 当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。
```tsx
function SearchPage() {
const [query, setQuery] = useState("");
@@ -88,16 +118,40 @@ function SearchPage() {
}
```
### Transition 与直接 setState 对比
### useTransition + Suspense 组合模式
```mermaid
timeline
title "输入 "hello" 的渲染行为"
直接 setState : 每次按键 → re-render\n(h/h/e/l/o 共 5 次)
useTransition : h,e,l,l → 跳过中间\no → 最终渲染一次
两者配合可以实现更精细的加载策略:Transition 控制哪个更新「可以等待」,Suspense 在数据就绪后展示最终内容。
```tsx
const [isPending, startTransition] = useTransition();
const deferredTab = useDeferredValue(activeTab);
return (
<>
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
{isPending && <PageSkeleton />}
<Suspense fallback={<SectionLoading />}>
<ActiveSection tab={deferredTab} />
</Suspense>
</>
);
```
### Transition vs DeferredValue 选择指南
| 场景 | 推荐 | 原因 |
|------|------|------|
| 路由切换后展示新视图 | `useTransition` | 明确标记整页过渡状态,配合 `<Suspense>` |
| 输入框即时搜索 + 延迟加载 | `useDeferredValue` | 一行搞定,自动处理防抖逻辑 |
| 多个状态联动更新 | `useTransition` | 更精确控制哪些 setState 属于低优先级 |
| 简单延迟一个值(如过滤条件) | `useDeferredValue` | 侵入性最小,不改变组件交互模式 |
| Tab 切换时内容区域的异步加载 | 两者组合 | `startTransition` 控制页面级 Pending,`useDeferredValue` 控制数据流 |
> [!tip] 核心区别一句话
>
> - **`useTransition`**:控制**「哪个 setState」**是低优先级的 → 你直接包裹需要降级的那段 setState 调用。
> - **`useDeferredValue`**:控制**「哪个值」**是延迟更新的 → 你用 `const delayed = useDeferredValue(value)` 拿到一个滞后 ~16ms 的副本。
## useDeferredValue —— 延迟副本
```tsx
@@ -117,14 +171,9 @@ function TodoApp() {
}
```
> [!tip] Transition vs DeferredValue 选择指南
> [!note] 内部机制
>
> | 场景 | 推荐 |
> |------|------|
> | 表单提交后展示新视图 | `useTransition` |
> | 输入框即时搜索 + 延迟加载 | `useDeferredValue` |
> | 多个状态联动更新 | `useTransition`(更明确控制粒度) |
> | 简单延迟一个值 | `useDeferredValue`(一行搞定) |
> `useDeferredValue` 内部等价于一次 `startTransition`。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。
## 时间切片原理
@@ -174,4 +223,46 @@ useEffect(() => {
// 用于发现不纯的 render 函数和清理逻辑缺失
```
## 最佳实践
> [!tip] 实战经验总结
### 代码示例:安全的 Effect Cleanup
```tsx
// ❌ 问题:异步 render → effect 执行时 dep 可能已变化
useEffect(() => {
const controller = new AbortController();
fetchData(url, { signal: controller.signal }).then(setData);
return () => controller.abort(); // cleanup 依赖的 url 可能已过时
}, [dep]);
// ✅ 推荐:用 ref 缓存最新值,保证 cleanup 拿到正确的信号
const latestSignalRef = useRef<AbortController>();
useEffect(() => {
const controller = new AbortController();
latestSignalRef.current = controller;
fetchData(url, { signal: controller.signal }).then(setData);
return () => {
if (latestSignalRef.current === controller) {
controller.abort(); // 确认是同一个请求才 abort
}
};
}, [dep]);
```
### 要点清单
1. **优先使用 Suspense,谨慎手动 startTransition**:Suspense 配合数据获取模式是 React 官方推荐的方向,而 `startTransition` 适合「一个交互触发多个 setState」的场景。
2. **避免在 render 中做副作用**:并发模式下 render 函数可能被多次调用、随时中断——render 应该是纯粹的「视图描述函数」。
3. **合理使用 `<Suspense>` boundary 层级**:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。
4. **用 `use()` + Suspense 替代 useEffect 中的数据获取**:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。
5. **警惕 `useEffect` 的异步执行时机**:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 `useSyncExternalStore` 或使用 ref 存储最新值。
## 关联笔记
- [[11-组件通信模式]]
- [[12-HOC 与 Render Props]]
- [[14-Serverside Rendering]]
+268 -29
View File
@@ -1,5 +1,5 @@
---
tags: [React, Next.js, SSR, SSG, ISR, Frontend]
tags: [React, Next.js, SSR, SSG, ISR, Frontend, Server Components]
create time: 2026-04-29 22:13
---
@@ -7,30 +7,34 @@ create time: 2026-04-29 22:13
## 概述
服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略与最佳实践。
服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略、Server Components 体系与最佳实践。
> [!question] 思考:用户从输入 URL 到看到页面,中间经历了哪些步骤?
>
> 传统 CSR 模式下,浏览器先拿到一个几乎空的 HTML,然后下载 JS bundle,执行 React 来生成内容——用户需要等待两件事都完成才能看到页面。SSR 把「生成 HTML」这一步搬到服务器做,用户请求回来时就已经有可读的内容了。
## 渲染模式对比
```mermaid
graph TB
subgraph "客户端渲染 CSR"
A[HTML空白页] --> B["下载 JS Bundle"]
B --> C["执行 React hydration"]
subgraph CSR["客户端渲染 CSR"]
A["HTML空白页"] --> B["下载JS Bundle"]
B --> C["执行React hydration"]
C --> D["显示内容"]
end
subgraph "服务端渲染 SSR"
E[请求页面] --> F["服务器渲染 React → HTML"]
F --> G["发送含内容的 HTML"]
G --> H["客户端 hydration"]
subgraph SSR["服务端渲染 SSR"]
E["请求页面"] --> F["服务器渲染React -> HTML"]
F --> G["发送含内容的HTML"]
G --> H["客户端hydration"]
H --> I["交互可用"]
end
subgraph "静态生成 SSG"
J["构建时渲染"] --> K["生成纯 HTML 文件"]
K --> L["CDN 分发"]
subgraph SSG["静态生成 SSG"]
J["构建时渲染"] --> K["生成纯HTML文件"]
K --> L["CDN分发"]
end
style F fill:#4FC08D,color:#fff
style H fill:#F5A87D,color:#000
style J fill:#61DAFB,color:#000
@@ -44,8 +48,18 @@ graph TB
| **SSG**(Static Site Generation) | 博客、文档、营销页 | ⏱️ 构建时生成 | ✅ |
| **ISR**(Incremental Static Regeneration) | 新闻列表、商品目录 | 🔄 定时后台更新 | ✅(增量) |
> [!tip] 核心区别一句话
>
> - **SSR**:每个用户请求都触发一次服务端的完整渲染。
> - **SSG**:构建时生成一次 HTML,所有用户共享同一个静态页面。
> - **ISR**:先用 SSG 生成的静态页面响应,后台静默重新生成后替换——对用户无感知。
## Next.js App Router 架构
> [!note] Server Component 是默认值
>
> Next.js App Router 中,所有组件默认就是 **Server Component**——除非你在文件顶部声明 `"use client"`。这意味着你可以放心地在组件里 await 数据库查询、读取环境变量或访问文件系统,这些代码永远不会发送到浏览器。
```tsx
// app/layout.tsx —— 根布局(所有页面共享)
export default function RootLayout({ children }: { children: React.ReactNode }) {
@@ -63,7 +77,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
async function HomePage() {
// ✅ 直接在组件中 await API
const posts = await fetchPosts();
return (
<main>
{posts.map(post => <PostCard key={post.id} post={post} />)}
@@ -84,17 +98,17 @@ async function PostPage({ params }: { params: { slug: string } }) {
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
Client->>Server: GET /dashboard
Server->>Server: 并行请求 user/profile/orders
Note over Server: 每个请求可有自己的 Suspense boundary
Server-->>Client: HTML: Navbar + Sidebar<br/>(立即显示,~200ms)
Server-->>Client: Stream: Profile card<br/>(中等优先级,~800ms)
Server-->>Client: Stream: Order history<br/>(低优先级,~1500ms)
Note over Server: 每个请求可有自己的Suspense boundary
Server-->>Client: HTML: Navbar + Sidebar<br/>(立即显示,约200ms)
Server-->>Client: Stream: Profile card<br/>(中等优先级,约800ms)
Server-->>Client: Stream: Order history<br/>(低优先级,约1500ms)
Note over Client: 用户体验:渐进式展示,而非等全部完成
```
@@ -107,9 +121,9 @@ function DashboardLayout({ children }: { children: React.ReactNode }) {
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
{children}
{/* 各区域独立 Suspense */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
@@ -122,6 +136,10 @@ function DashboardLayout({ children }: { children: React.ReactNode }) {
}
```
> [!note] Streaming SSR 的工作原理
>
> 服务端将 HTML 分成多个 chunk,通过网络流逐步发送给浏览器。浏览器边收边渲染——不需要等所有数据就绪。**优先级高的区域先出,低的在后**,用户体验从「全部等」变成「渐进可见」。
## Data Fetching 策略
```tsx
@@ -151,7 +169,16 @@ function Page() {
}
```
## SSR vs Client Component 边界
### Cache vs Revalidate vs no-store 决策指南
| 策略 | 行为 | 适合场景 |
|------|------|---------|
| `fetch(url)` 不加配置 | 基于 HTTP 协议的永久缓存(直到下次部署失效) | 不常变化的配置数据 |
| `{ next: { revalidate: N } }` | CDN 级别缓存,N 秒后后台重新验证 | 商品信息、文章列表等 |
| `{ cache: "no-store" }` | 完全不走缓存,每次都回源 | 用户面板、实时仪表盘 |
| `{ next: { tags: ["posts"] } }` + `revalidateTag("posts")` | 按标签精确失效 | CMS 系统发布新文章时触发更新 |
## Server Component vs Client Component 边界
```tsx
// server component(默认,无需声明)
@@ -168,7 +195,7 @@ function InteractiveChart() {
// ✅ 可以使用 useState/useEffect/DOM API
const [zoom, setZoom] = useState(1);
useEffect(() => { ... }, []);
return <canvas ref={canvasRef} />;
}
@@ -184,7 +211,219 @@ function Dashboard() {
```
> [!warning] Client → Server 通信限制
>
> - Client Component 无法直接调用 Server Component 的方法或 props
> - Server Component 也不能传给 Client Component 引用了函数、Promise 或 Generator 的值
> - 解决方案:通过 URL 参数、cookies、或后端 API 传递数据
### 何时该用 Server Component?何时该用 Client Component?
> [!question] 如何判断一个组件应该放在哪一边?
>
> 记住一条黄金法则:**能放服务器的就放服务器**。Server Component 是零 bundle size 的——它们不会增加客户端 JavaScript 体积。只有当你确实需要浏览器专属 API(事件监听、状态管理、DOM 操作)时才降级到 Client Component。
| 需要浏览器能力吗? | 推荐方案 |
|-------------------|---------|
| ❌ 不需要(只渲染数据) | Server Component ✅ |
| ✅ 需要 `useState` / `useEffect` / `onClick` | Client Component (`"use client"`) |
| ✅ 需要第三方交互库(地图、图表) | Client Component |
| ✅ 需要浏览器 API(localStorage、Geolocation) | Client Component |
| ❌ 只需要从 API 获取数据并展示 | Server Component ✅ |
## Server Actions
Server Action 允许你在服务端定义可直接从 Client Component 调用的异步函数——无需手动写 API 路由。
```tsx
// app/actions.ts —— 独立的 Server Action 模块
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// 数据库写入
await db.post.create({ data: { title, content } });
// 成功后刷新对应页面的缓存
revalidatePath("/blog");
}
```
```tsx
// app/blog/new/page.tsx —— 表单页面(Client Component)
"use client";
import { createPost } from "@/app/actions";
function NewPostForm() {
async function handleSubmit(formData: FormData) {
await createPost(formData);
// 提交后跳转到列表页
router.push("/blog");
}
return (
<form action={handleSubmit}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
);
}
```
> [!tip] Server Actions 的优势
>
> 1. **零样板代码**:不再需要手动编写 API Route + fetch 调用链
> 2. **类型安全**:函数签名天然携带类型信息
> 3. **内置序列化和校验**:FormData 自动解析,配合 Zod 做 schema 校验
> 4. **直接操作服务端状态**:数据库读写、文件操作、认证上下文一步到位
## Metadata API & 动态 SEO
Next.js 提供了声明式的元数据 API,自动生成 `<head>` 中的标签。
```tsx
// app/blog/[slug]/page.tsx —— 动态元数据
import { getPostBySlug } from "@/lib/posts";
// 静态 generateMetadata(构建时可确定)
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
openGraph: { title, images: [post.coverImage] },
};
}
// 动态 generateViewport / robots / sitemap
export function generateStaticParams() {
const posts = getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
```
> [!note] generateStaticParams vs dynamicParams
>
> 如果未列出的动态路由被访问到,Next.js 默认会返回 404。你可以通过 `next.config.js` 设置 `dangerouslyAllowHostnameMismatch = true` 或在路由目录中添加 `not-found.tsx` 来自定义 404 行为。对于 API 驱动的项目,可以设置 `dynamicParams = false` 确保安全性。
## Cookies、Headers 与 Auth
Server Component 可以直接读取和修改 cookies/headers,这是构建认证系统的基石。
```tsx
// app/dashboard/page.tsx —— Server Component
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
async function DashboardPage() {
// 读取 cookie
const sessionCookie = (await cookies()).get("session");
// 验证 token
if (!sessionCookie) {
redirect("/login");
}
// 读取请求头
const userAgent = (await headers()).get("user-agent");
// 使用 session 数据渲染页面
return <DashboardContent session={sessionCookie.value} />;
}
```
## 错误处理
SSR 环境下的错误处理需要考虑服务端和网络异常的双重场景。
```tsx
// app/error.tsx —— 捕获渲染阶段的错误
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>出错了</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
);
}
// app/not-found.tsx —— 自定义 404 页面
export default function NotFound() {
return <h2>页面不存在</h2>;
}
// 异步组件中的优雅降级
async function CommentsSection({ postId }: { postId: string }) {
// 评论组件失败不影响整个页面
let comments;
try {
comments = await fetchComments(postId);
} catch {
comments = []; // 降级为空数组
}
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
```
## 性能调优
### 关键指标与优化方向
```mermaid
graph LR
A["TTFB<br/>Time to First Byte"] -->|"降低服务器处理时间"| B["边缘缓存<br/>ISR / CDN"]
C["LCP<br/>Largest Contentful Paint"] -->|"减小首屏 JS 体积"| D["Server Components ✅"]
E["CLS<br/>Cumulative Layout Shift"] -->|"预留空间防抖动"| F["固定尺寸 + Aspect Ratio"]
style A fill:#F5A87D,color:#000
style C fill:#F5A87D,color:#000
style E fill:#F5A87D,color:#000
```
### Image Optimization & Font Optimization
```tsx
import Image from "next/image";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] }); // 零 CLS 字体加载
// next/image 自动做 WebP 转换 + responsive srcset + lazy loading
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
placeholder="blur" // 懒加载时用模糊缩略图占位
priority // 首屏图片提前加载
/>
```
## 最佳实践
> [!tip] 实战经验总结
### 要点清单
1. **Server Component 优先**:默认情况下所有组件都是 Server Component,充分利用其零 bundle size 和数据直取的能力。只有在需要交互时才添加 `"use client"`。
2. **Suspense 粒度要合理**:不要把所有东西包在一个大 Suspense 里(起不到流式效果),也不要每个小组件都加(overhead)。按数据依赖层级划分边界。
3. **合理使用数据缓存策略**:`revalidate` 适合大多数内容型数据,`no-store` 用于用户相关实时数据,`tags` 用于精确实时失效控制。
4. **Server Actions 替代手写 API**:减少样板代码的同时保持类型安全,但注意不要滥用——简单 CRUD 以外仍建议走标准的 API Route 模式。
5. **做好错误降级**:SSR 中一个组件出错会导致整页白屏,对非关键组件要用 try-catch 兜底或 `<Suspense>` 隔离。
6. **善用 Metadata API**:SEO 相关的工作都应该在 `generateMetadata` 等 API 中完成,避免在 JSX 中手动操作 head。
7. **关注 Core Web Vitals**:Server Components 天然利好 LCP(减少 JS),ISR + CDN 利好 TTFB,Image/Font Optimization 利好 CLS。
## 关联笔记
- [[13-并发特性]]
- [[15-性能优化]]
- [[09-状态管理]]