((props, ref) => {
validate: () => validator.validate(),
reset: () => setFields(initialValues),
}));
-
+
return ;
});
```
+> [!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["仅父子两层
(1-2 级深度)"]
+ RANGE --> DEEP["多层级穿透
(≥ 3 级深度)"]
+ RANGE --> SIBLING["兄弟组件 / 无关组件"]
+ RANGE --> IMPERATIVE["需命令式调用子组件方法"]
+
+ PARENT_CHILD --> PROP["Props + Callback ✅
简单、直观、可追踪"]
+ DEEP --> CTX["Context ✅
配合 useReducer 保证性能"]
+ SIBLING --> SLIFTING["状态提升到共同父 ✅"]
+ SIBLING --> STORE["状态管理库(复杂场景)"]
+ IMPERATIVE --> REF["forwardRef +
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 分析
diff --git a/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
index ed38a19..7abd2af 100644
--- a/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
+++ b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
@@ -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(Wrapped: React.ComponentType
) {
+ const WithAuth: React.FC
= (props) => {
+ // ... 认证逻辑
+ return ;
+ };
+ WithAuth.displayName = `withAuth(${Wrapped.name || 'Component'})`;
+ return WithAuth;
+}
+// DevTools 中显示:withAuth(Dashboard) → 一目了然
+```
+
+### 常见 HOC 模式
+
+#### 1. 日志 / HUD
+
+```tsx
+// 通用:为组件自动记录生命周期事件
function withLogger
(Comp: React.FC
): React.FC
{
return function LoggedComponent(props: P) {
- useEffect(() => console.log(`${Comp.name} mounted`), []);
+ useEffect(() => console.log(`[Mount] ${Comp.name || 'Anonymous'}`), []);
return ;
};
}
+```
+> [!tip] 解释
+> 不传任何额外 prop,只"旁路"添加副作用(如日志)。这是 HOC 最轻量的用法——原组件完全不知情。
-// 2. 加载态封装
+#### 2. 加载态封装
+
+```tsx
+// 自动管理 loading 状态 + 错误处理
function withLoading
(
Comp: React.FC
,
fetchData: () => Promise
): React.FC {
return function LoadingWrapper(props: P) {
const [loading, setLoading] = useState(true);
- useEffect(() => { fetchData().then(() => setLoading(false)); }, []);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchData().then(() => setLoading(false)).catch(setError);
+ }, []);
+
+ if (error) return ;
return loading ? : ;
};
}
+```
+> [!note] 解释
+> HOC 在**不改动组件渲染逻辑**的前提下,统一接管了 "加载中 / 出错 / 成功" 三种 UI 状态。多个页面复用同一套加载策略,DRY。
-// 3. Props 转换(驼峰→kebab)
+#### 3. Props 转换
+
+```tsx
+// 将 props 做格式变换后注入子组件
function withPropsTransformer(
Comp: React.FC
,
transform: (p: P) => Record
): React.FC {
return function Transformed(props: P) {
const extra = transform(props);
- return ;
+ // ⚠️ transform 返回的 key 可能与原 props 冲突
+ return ;
};
}
```
+> [!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(Wrapped: React.ComponentType
) {
-> return React.forwardRef((props, ref) => {
-> const { authenticated } = useAuth();
-> if (!authenticated) return ;
-> return ;
-> });
-> }
-> ```
+#### 为什么需要 forwardRef?
+
+```tsx
+export function withAuth(Wrapped: React.ComponentType
) {
+ return React.forwardRef((props, ref) => {
+ const { authenticated } = useAuth();
+ if (!authenticated) return ;
+ return ;
+ });
+}
+```
+> [!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 {render(pos)}
;
}
@@ -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
;
```
+> [!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 (
+
+ {slides.map((s, i) => renderItem(s, i))}
+
+ {slides.map((_, i) => renderIndicator?.(i, i === active) ?? (
+ setActive(i)}
+ className={i === active ? 'dot-active' : 'dot'} />
+ ))}
+
+
+ );
+}
+```
+> [!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
函数包裹组件"]
+ B --> D["Render Props
函数传递 prop"]
+ B --> E["Custom Hook
纯函数抽离逻辑"]
- 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(); // 数据缓存
@@ -190,12 +318,24 @@ function UserList() {
return ;
}
-// 扁平可读、天然共享 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 模式]]
\ No newline at end of file
diff --git a/hhs/REACT/4. 进阶篇/13-并发特性.md b/hhs/REACT/4. 进阶篇/13-并发特性.md
index 63f73c9..04ff18e 100644
--- a/hhs/REACT/4. 进阶篇/13-并发特性.md
+++ b/hhs/REACT/4. 进阶篇/13-并发特性.md
@@ -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 + SSR(流式渲染)
+
+> [!tip] React 18 的 Streaming SSR
+>
+> Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。
+
+```tsx
+// 服务端组件中嵌套 Suspense boundary
+export default async function Page() {
+ return (
+
+ {/* 同步渲染 */}
+ }>
+ {/* 等待 fetch 完成后插入 */}
+
+
+ );
+}
+```
+
+服务端输出变成**渐进式 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 (
+ <>
+
+ {isPending && }
+ }>
+
+
+ >
+);
```
+### Transition vs DeferredValue 选择指南
+
+| 场景 | 推荐 | 原因 |
+|------|------|------|
+| 路由切换后展示新视图 | `useTransition` | 明确标记整页过渡状态,配合 `` |
+| 输入框即时搜索 + 延迟加载 | `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();
+
+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. **合理使用 `` boundary 层级**:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。
+4. **用 `use()` + Suspense 替代 useEffect 中的数据获取**:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。
+5. **警惕 `useEffect` 的异步执行时机**:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 `useSyncExternalStore` 或使用 ref 存储最新值。
+
## 关联笔记
+- [[11-组件通信模式]]
+- [[12-HOC 与 Render Props]]
+- [[14-Serverside Rendering]]
diff --git a/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md
index 5e10b9e..0a82bb1 100644
--- a/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md
+++ b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md
@@ -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 (
{posts.map(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
(立即显示,~200ms)
-
- Server-->>Client: Stream: Profile card
(中等优先级,~800ms)
-
- Server-->>Client: Stream: Order history
(低优先级,~1500ms)
-
+ Note over Server: 每个请求可有自己的Suspense boundary
+
+ Server-->>Client: HTML: Navbar + Sidebar
(立即显示,约200ms)
+
+ Server-->>Client: Stream: Profile card
(中等优先级,约800ms)
+
+ Server-->>Client: Stream: Order history
(低优先级,约1500ms)
+
Note over Client: 用户体验:渐进式展示,而非等全部完成
```
@@ -107,9 +121,9 @@ function DashboardLayout({ children }: { children: React.ReactNode }) {
}>
-
+
{children}
-
+
{/* 各区域独立 Suspense */}
}>
@@ -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 ;
}
@@ -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 (
+
+ );
+}
+```
+
+> [!tip] Server Actions 的优势
+>
+> 1. **零样板代码**:不再需要手动编写 API Route + fetch 调用链
+> 2. **类型安全**:函数签名天然携带类型信息
+> 3. **内置序列化和校验**:FormData 自动解析,配合 Zod 做 schema 校验
+> 4. **直接操作服务端状态**:数据库读写、文件操作、认证上下文一步到位
+
+## Metadata API & 动态 SEO
+
+Next.js 提供了声明式的元数据 API,自动生成 `` 中的标签。
+
+```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 ;
+}
+```
+
+## 错误处理
+
+SSR 环境下的错误处理需要考虑服务端和网络异常的双重场景。
+
+```tsx
+// app/error.tsx —— 捕获渲染阶段的错误
+"use client";
+
+export default function Error({ error, reset }: { error: Error; reset: () => void }) {
+ return (
+
+
出错了
+
{error.message}
+
+
+ );
+}
+
+// app/not-found.tsx —— 自定义 404 页面
+export default function NotFound() {
+ return 页面不存在
;
+}
+
+// 异步组件中的优雅降级
+async function CommentsSection({ postId }: { postId: string }) {
+ // 评论组件失败不影响整个页面
+ let comments;
+ try {
+ comments = await fetchComments(postId);
+ } catch {
+ comments = []; // 降级为空数组
+ }
+
+ return {comments.map(c => - {c.text}
)}
;
+}
+```
+
+## 性能调优
+
+### 关键指标与优化方向
+
+```mermaid
+graph LR
+ A["TTFB
Time to First Byte"] -->|"降低服务器处理时间"| B["边缘缓存
ISR / CDN"]
+ C["LCP
Largest Contentful Paint"] -->|"减小首屏 JS 体积"| D["Server Components ✅"]
+ E["CLS
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
+
+```
+
+## 最佳实践
+
+> [!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 兜底或 `` 隔离。
+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-状态管理]]
diff --git a/hhs/REACT/5. 工程实践篇/15-性能优化.md b/hhs/REACT/5. 工程实践篇/15-性能优化.md
index 780f32a..120ae2a 100644
--- a/hhs/REACT/5. 工程实践篇/15-性能优化.md
+++ b/hhs/REACT/5. 工程实践篇/15-性能优化.md
@@ -9,6 +9,42 @@ create time: 2026-04-29 22:14
React 性能优化的核心原则是 **"减少不必要的渲染"**。本文档从诊断工具到具体手段,提供完整的调优方法论。
+> [!question] 思考:为什么 React 会慢?
+> 大多数情况下,应用变慢不是因为 React 本身慢,而是因为 **它重新渲染了不该渲染的组件**。
+> 一个常见的陷阱:父组件 state 变化 → 所有子组件重新执行 render(即使它们的 props 没变)。
+> 我们的目标:让 React 只渲染真正需要更新的部分。
+
+### 优化决策流程图
+
+```mermaid
+flowchart TD
+ A[页面卡顿?] --> B{使用 Profiler 定位}
+ B --> C[频繁重渲染?]
+ B --> D[首屏加载慢?]
+ B --> E[滚动掉帧?]
+
+ C --> F[props 不变却渲染?]
+ C --> G[状态更新频率过高?]
+ F --> H["React.memo / useMemo"]
+ G --> I["useCallback / 防抖节流"]
+
+ D --> J[打包体积过大?]
+ D --> K[某些功能很少用?]
+ J --> L["Code Splitting / Tree Shaking"]
+ K --> M["Dynamic Import"]
+
+ E --> N[列表项很多?]
+ N --> O["虚拟列表 Virtual Scroll"]
+
+ style H fill:#61DAFB,color:#000
+ style L fill:#61DAFB,color:#000
+ style O fill:#61DAFB,color:#000
+```
+
+> [!warning] 第一条原则:先测量,再优化
+> 没有 Profiler 数据的优化都是猜。先用 React DevTools 或 Chrome Performance 确认瓶颈在哪,
+> 然后**针对性地解决**,而不是盲目地在每个组件上加 memo。
+
## 性能诊断工具箱
```mermaid
@@ -22,7 +58,7 @@ graph TB
C --> G["定位重渲染的组件和原因"]
D --> H["分析主线程阻塞时段"]
- E --> I["整体 LCP/FID/CLS 评分"]
+ E --> I["整体 LCP/INP/CLS 评分"]
F --> J["生产环境真实用户数据"]
style C fill:#61DAFB,color:#000
@@ -54,6 +90,64 @@ const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
> - 父组件每次传新对象/函数作 prop → memo 无效
> - 简单列表(几十项以内)不需要 memo
+## 缓存计算与回调 — useMemo / useCallback
+
+React.memo 解决的是"组件不重新渲染",而 `useMemo` 和 `useCallback` 解决的是**"值不重新生成"**。
+
+### useMemo —— 缓存昂贵的计算结果
+
+```tsx
+function SearchPage({ items, keyword }: { items: string[]; keyword: string }) {
+ // 过滤逻辑只在 items 或 keyword 变化时重新执行
+ const filtered = useMemo(() => {
+ console.log("filtering..."); // 仅依赖变化时才打印
+ return items.filter(item => item.includes(keyword));
+ }, [items, keyword]);
+
+ return
;
+}
+```
+
+> [!tip] useMemo 的本质
+> 它不是"性能捷径",而是**跳过不必要的计算**。对于 O(n) 以下的轻量操作,useMemo 反而增加内存开销。
+> **适用场景**:大数据量处理、深度嵌套对象的派生计算、API 请求参数校验。
+
+### useCallback —— 缓存函数引用
+
+```tsx
+function Parent() {
+ const [count, setCount] = useState(0);
+
+ // count 变化时才创建新函数,Child 用 React.memo 包裹时不会被重渲染
+ const handleClick = useCallback(() => {
+ setCount(c => c + 1);
+ }, []);
+
+ return (
+ <>
+
+ {/* 不受 count 变化影响 */}
+ >
+ );
+}
+```
+
+> [!important] 什么时候需要 useCallback?
+> 仅在以下两种场景中使用:
+> 1. 函数传给 `React.memo` 包裹的子组件,避免父状态变化导致子组件重渲染
+> 2. 作为其他 Hook(如 `useEffect`)的依赖项
+>
+> 否则——不加更简单,也更容易维护。
+
+---
+
+> [!question] 思考:三个 memo 的关系
+> - **React.memo**: 阻止组件树 re-render(组件级别)
+> - **useMemo**: 缓存返回值(值级别)
+> - **useCallback**: 缓存函数引用(等价于 `useMemo(() => fn, deps)`)
+>
+> 它们共同目标是:**缩小变更传播范围**。当 state 更新时,让影响尽可能少地扩散到子树。
+
## 虚拟列表(Virtual Scrolling)
当列表项超过数百条时,虚拟滚动通过只渲染可视区域内的 DOM 元素来大幅降低内存占用:
@@ -103,39 +197,69 @@ function VirtualList({ items }: { items: string[] }) {
## Code Splitting
+Code Splitting 的核心理念:**用户不需要一次性下载所有代码**。按需加载可以显著降低首屏时间。
+
### 路由级拆分
```tsx
-// Next.js App Router(内置)
-const AdminPage = lazy(() => import("./pages/Admin"));
+import { lazy, Suspense } from "react";
+import { BrowserRouter, Routes, Route } from "react-router-dom";
-// vite + React Router
-const Settings = lazy(() => import(/* vite: preload */ "./pages/Settings"));
+// 每个路由对应一个 chunk,用户只下载当前页面对应的代码
+const Home = lazy(() => import("./pages/Home"));
+const Admin = lazy(() => import("./pages/Admin"));
+
+function App() {
+ return (
+
+ }>
+
+ } />
+ } />
+
+
+
+ );
+}
```
+> [!note] Next.js 的特殊之处
+> Next.js 基于文件系统自动实现路由级 Code Splitting,**无需手动 lazy()**。
+> 在 `/app` 目录中,每个页面文件就是一个独立的 route segment → 自动拆包。
+
### 组件级拆分
+对体积大、使用频率低的第三方库进行动态导入:
+
```tsx
import dynamic from "next/dynamic";
-// 不加载图表库直到真正需要
+// SSR 关闭——图表库依赖 window,服务端没有
const Chart = dynamic(() => import("recharts"), { ssr: false });
-// 或带 loading fallback
-const HeavyEditor = dynamic(() => import("@monaco-editor/react"), {
- loading: () => Loading editor...
,
- ssr: false,
-});
+
+// 带 loading fallback——提升感知体验
+const HeavyEditor = dynamic(
+ () => import("@monaco-editor/react"),
+ { loading: () => 正在加载编辑器...
, ssr: false }
+);
+
+// React 原生写法——配合 Suspense
+const MapComponent = lazy(() => import("./MapComponent"));
+// 可指定 preloadStrategy——预加载策略
+const PrefetchModal = lazy(
+ () => import("./HeavyModal")
+);
```
### Bundle 分析与优化
```bash
-# 安装插件
-npm install --save-dev rollup-plugin-visualizer
-# 或在 Next.js 中使用 next-bundle-analyzer
+# Next.js 项目
+npx next-bundle-analyzer
-# 生成可视化报告
-npx run build && npx visualizer
+# Vite / Webpack 通用方案
+npm install --save-dev rollup-plugin-visualizer
+npm run build && npx visualizer
```
> [!tip] Bundle 大小目标
@@ -145,13 +269,83 @@ npx run build && npx visualizer
> | 单个 chunk | < 300KB |
> | JS Total | < 500KB(SPA)/ < 200KB(PWA) |
+> [!tip] 减小 bundle 的三个高效手段
+> 1. **替换大库**——`lodash` → `lodash-es`(tree-shaking 友好),或直接用原生 API
+> 2. **检查重复依赖**——`npm ls react` 看有没有多份 React 实例
+> 3. **静态资源不进 bundle**——图片、视频等上传到 CDN,URL 写在代码里
+
## Lighthouse 关键指标调优
| 指标 | 含义 | 优化方向 |
|------|------|----------|
-| **FCP**(First Contentful Paint) | 首次内容绘制 | 减小首屏 HTML/JS 体积 |
-| **LCP**(Largest Contentful Paint) | 最大内容绘制 | 图片懒加载、预加载关键资源 |
-| **INP**(Interaction to Next Paint) | 交互响应延迟 | useTransition、删除同步 heavy work |
-| **CLS**(Cumulative Layout Shift) | 布局偏移 | 预留图片宽高、避免字体闪烁 |
+| **FCP**(First Contentful Paint) | 首次内容绘制 | 减小首屏 HTML/JS 体积、使用 CDN |
+| **LCP**(Largest Contentful Paint) | 最大内容绘制 | 图片懒加载、预加载关键资源、优先渲染 |
+| **INP**(Interaction to Next Paint) | 交互响应延迟(取代 FID) | useTransition、删除同步 heavy work、Web Worker 分流 |
+| **CLS**(Cumulative Layout Shift) | 布局偏移 | 预留图片宽高、避免字体闪烁、占位广告容器 |
+
+## React 并发优化 —— startTransition / useDeferredValue
+
+React 18 引入的并发特性是解决 **"交互卡顿"** 最直接的手段。它们让 React 能够**优先级调度**。
+
+```tsx
+import { useState, startTransition } from "react";
+
+function SearchApp() {
+ const [query, setQuery] = useState("");
+ const [results, setResults] = useState([]);
+
+ // ❌ 普通 setState:阻塞 UI,用户输入时感知到延迟
+ // setSearchResults(heavySearch(query));
+
+ // ✅ startTransition:标记为低优先级,不阻塞当前输入
+ function handleChange(e: string) {
+ setQuery(e); // 高优先级——立即更新
+ startTransition(() => {
+ setResults(heavySearch(e)); // 低优先级——可以中断
+ });
+ }
+
+ return ;
+}
+```
+
+### 与 Suspense 配合的最佳实践
+
+```tsx
+// 路由级懒加载 + 过渡状态
+}>
+
+
+ } />
+
+
+```
+
+> [!tip] startTransition vs useDeferredValue 的选择
+> | 场景 | 推荐方案 |
+> |------|----------|
+> | 表单输入 → 搜索结果 | `startTransition` |
+> | 长列表滚动 → 非关键区域降级 | `useDeferredValue` |
+> | 异步数据加载(路由级) | `Suspense` + `lazy` |
+> | 复杂计算(不在渲染中) | Web Worker / TanStack Query |
+
+---
+
+> [!summary] 性能优化 checklist
+> - [ ] 用 Profiler 确认瓶颈位置
+> - [ ] 高频重渲染组件加 `React.memo`
+> - [ ] 派生计算用 `useMemo`,回调用 `useCallback`
+> - [ ] 大数据列表用虚拟滚动
+> - [ ] 大库和低频页面做 Code Splitting
+> - [ ] 耗时操作放进 `startTransition`
+> - [ ] Lighthouse 评分达标?Web Vitals 监控上线?
## 关联笔记
+
+- [[06-性能优化 Hooks]] — useMemo、useCallback、useTransition 等 Hook 的详细用法
+- [[13-并发特性]] — React 18 并发渲染机制深入解析
+- [[04-State 与不可变性]] — State 更新模式对性能的影响
+- [[09-状态管理]] — 全局状态管理的性能考量(Zustand / Redux / Jotai)
+- [[14-Serverside Rendering]] — SSR / RSC 的首屏性能优势
+- [[16-测试]] — 性能回归测试:React Testing Library + Performance Assertions
diff --git a/hhs/REACT/5. 工程实践篇/16-测试.md b/hhs/REACT/5. 工程实践篇/16-测试.md
index 547b10a..f2d66e5 100644
--- a/hhs/REACT/5. 工程实践篇/16-测试.md
+++ b/hhs/REACT/5. 工程实践篇/16-测试.md
@@ -146,11 +146,84 @@ describe("", () => {
});
```
+### 快照测试
+
+> [!tip] Snapshot 的定位
+> 快照不是单元测试的替代品——它检测的是**UI 结构意外变化**。
+> 适用于不会频繁变化的展示型组件(如仪表盘卡片、文章详情页)。
+> 不适用于动态数据多的列表或表单。
+
+```tsx
+import { render } from "@testing-library/react";
+import { UserDetail } from "./UserDetail";
+
+it("渲染与之前一致", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+});
+```
+
+> [!warning] 快照的陷阱
+> - 时间戳、随机 ID 等动态内容会导致每次快照不同 → 用 `sanitize` 处理
+> - 盲目更新快照(`-u`)等于放弃测试价值 → **每次 diff 都要人工审查**
+> - 配合确定性断言一起使用,不要只做快照断言
+
+### 查询方法速查表
+
+> [!question] 思考:getBy、queryBy、findBy 我该用哪个?
+> React Testing Library 提供三类查询,它们的**行为差异直接决定测试的健壮性**。
+
+| 前缀 | 匹配不到时 | 典型场景 |
+|------|-----------|---------|
+| `getBy*` | **抛异常**(测试失败) | 验证元素**必须存在** |
+| `queryBy*` | 返回 `null` | 验证元素**不应存在**(配合 `.not.toBeInTheDocument()`) |
+| `findBy*` | **超时后抛异常** | 等待**异步出现**的元素(内部自动 await) |
+
+```tsx
+// getBy — 必须有,没有就失败(同步)
+const submitBtn = screen.getByRole("button", { name: /submit/i });
+
+// queryBy — 验证"不存在"(同步,推荐优先于 findBy 做否定断言)
+expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+
+// findBy — 等待异步出现的元素(内部封装了 waitFor + polling)
+const successMsg = await screen.findByText("Saved!");
+
+// ✅ 避免:手动写 waitFor + getBy(findBy 更简洁)
+// await waitFor(() => expect(screen.getByText("Saved!")).toBeInTheDocument());
+
+// getAllBy — 匹配多个元素
+const checkboxes = screen.getAllByRole("checkbox");
+expect(checkboxes).toHaveLength(3);
+```
+
+> [!tip] 查询优先级原则
+> 按以下顺序选择查询方式,越靠前的优先级越高:
+>
+> `role` → `label` → `text` → `testID` → `screen`(最后手段)
+>
+> **永远不要**用 `container.querySelector(".css-class")`——样式变了测试就挂了。
+
+---
+
## Mock 异步操作
+### vi.mock() — 模块级 Mock
+
+```tsx
+// 内联 Mock,隔离 API 依赖
+vi.mock("../api/user", () => ({
+ getUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
+ updateUser: vi.fn().mockResolvedValue(undefined),
+}));
+```
+
+### Mock Fetch / Axios
+
```tsx
it("loading 态在请求完成后消失", async () => {
- vi.mocked(fetch).mockResolvedValueOnce({
+ // 推荐:用 vi.spyOn 包装全局 fetch
+ vi.spyOn(global, "fetch").mockResolvedValueOnce({
ok: true,
json: async () => [{ id: 1, name: "Test" }],
} as Response);
@@ -167,7 +240,7 @@ it("loading 态在请求完成后消失", async () => {
});
it("网络错误显示错误提示", async () => {
- vi.mocked(fetch).mockRejectedValueOnce(new Error("Network error"));
+ vi.spyOn(global, "fetch").mockRejectedValueOnce(new Error("Network error"));
render();
@@ -177,6 +250,13 @@ it("网络错误显示错误提示", async () => {
});
```
+> [!note] API Mock 方案选型
+> - **vi.mock() / vi.spyOn**:适合单元测试,快速隔离依赖,但无法模拟完整网络流程
+> - **MSW (Mock Service Worker)**:通过 Service Worker 拦截真实网络请求,适用于集成测试;与生产行为最接近
+> - **建议**:组件层用 vi.mock;集成流用 MSW
+
+---
+
## Hook 测试
```tsx
@@ -188,9 +268,10 @@ describe("useDebounce", () => {
afterEach(() => vi.useRealTimers());
it("值不变时返回原始值", () => {
- const { result } = renderHook(({ value }) => useDebounce(value, 300), {
- initialProps: { value: "hello", delay: 300 },
- });
+ const { result } = renderHook(
+ ({ value }) => useDebounce(value, 300),
+ { initialProps: { value: "hello" } }
+ );
expect(result.current).toBe("hello");
});
@@ -210,6 +291,100 @@ describe("useDebounce", () => {
});
```
+> [!tip] Hook 测试的关键模式
+> - **时间控制**:用 `vi.useFakeTimers()` + `act(() => vi.advanceTimersByTime(n))` 精确控制异步时序,不依赖真实等待
+> - **cleanup**:每个 `beforeEach` 对应独立的渲染上下文,`rerender` 模拟 props 更新时的行为
+> - **不要断言内部变量**:只检查 `result.current`(返回值),就像组件只暴露 props 一样
+
+---
+
+## 集成测试模式
+
+```tsx
+// 模拟一个完整的用户操作流程
+describe(" — 集成测试", () => {
+ it("完整注册流程:填写 → 提交 → 跳转", async () => {
+ // 1. Mock API 响应
+ vi.mock("../api/auth", () => ({
+ register: vi.fn().mockResolvedValue({ token: "abc123" }),
+ }));
+
+ // 2. 渲染表单
+ render();
+
+ // 3. 模拟用户输入(userEvent 更符合真实行为)
+ const emailInput = screen.getByLabelText(/email/i);
+ const passwordInput = screen.getByLabelText(/password/i);
+
+ await userEvent.type(emailInput, "alice@example.com");
+ await userEvent.type(passwordInput, "SecurePass123!");
+
+ // 4. 提交
+ const submitBtn = screen.getByRole("button", { name: /sign up/i });
+ await userEvent.click(submitBtn);
+
+ // 5. 验证结果
+ await screen.findByText("Account created!");
+ });
+
+ it("重复邮箱提示错误", async () => {
+ vi.mock("../api/auth", () => ({
+ register: vi.fn().mockRejectedValue(new Error("Email already exists")),
+ }));
+
+ render();
+
+ const emailInput = screen.getByLabelText(/email/i);
+ await userEvent.type(emailInput, "exists@example.com");
+ await userEvent.click(screen.getByRole("button", { name: /sign up/i }));
+
+ await screen.findByText("Email already exists");
+ });
+});
+```
+
+> [!important] 集成测试的设计要点
+> - **一次测一个完整流程**,不要拆成原子步骤——这样即使中间环节变动,只要最终行为一致就不需要改测试
+> - **Mock 所有外部依赖**(API、localStorage、WebSocket),但不 mock 内部逻辑
+> 集成测试的目标是:**确认多个组件协同工作后,用户得到了正确的反馈**
+
+---
+
+## 错误边界测试
+
+```tsx
+import { ErrorBoundary } from "./ErrorBoundary";
+
+describe("", () => {
+ // 让被包裹的组件抛出错误
+ function BrokenComponent() {
+ throw new Error("Render failed");
+ }
+
+ it("捕获渲染错误并显示 fallback UI", () => {
+ render(
+ Something went wrong}>
+
+
+ );
+
+ expect(screen.getByText("Something went wrong")).toBeInTheDocument();
+ });
+
+ it("未发生错误时正常渲染子内容", () => {
+ render(
+
+ Safe content
+
+ );
+
+ expect(screen.getByText("Safe content")).toBeInTheDocument();
+ });
+});
+```
+
+---
+
## E2E 测试选择
| 工具 | 适用场景 | 特点 |
@@ -222,4 +397,11 @@ describe("useDebounce", () => {
> - E2E 只应覆盖**关键用户旅程**(登录、下单、支付)
> - 不要为每个页面的每个字段写 E2E——那属于集成测试的范畴
+---
+
## 关联笔记
+
+- [[07-自定义 Hooks]] — 自定义 Hook 的设计与测试模式
+- [[09-状态管理]] — 全局状态管理(Zustand / Redux Toolkit)的测试策略
+- [[13-并发特性]] — Suspense、Transitions 的测试注意事项
+- [[15-性能优化]] — 性能回归测试:React Testing Library + Performance Assertions
diff --git a/hhs/REACT/5. 工程实践篇/17-可访问性.md b/hhs/REACT/5. 工程实践篇/17-可访问性.md
index 14f5f36..0ff3e4b 100644
--- a/hhs/REACT/5. 工程实践篇/17-可访问性.md
+++ b/hhs/REACT/5. 工程实践篇/17-可访问性.md
@@ -38,8 +38,18 @@ graph TB
## 语义化 HTML(最重要的一条)
+> [!tip] 黄金法则
+> **能用原生元素,绝不用 div + onClick。** 原生 HTML 元素天生具备键盘交互、屏幕阅读器支持和 SEO 友好能力。这是可访问性的基石,比任何 ARIA 属性都重要。
+
+思考:为什么一个 `` 加上 `onClick` 事件后,仍然不能代替 `