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
+214 -20
View File
@@ -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 <List data={filtered} />;
}
```
> [!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 (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onAction={handleClick} /> {/* 不受 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 (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
```
> [!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: () => <p>Loading editor...</p>,
ssr: false,
});
// 带 loading fallback——提升感知体验
const HeavyEditor = dynamic(
() => import("@monaco-editor/react"),
{ loading: () => <p>正在加载编辑器...</p>, 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 <input value={query} onChange={handleChange} />;
}
```
### 与 Suspense 配合的最佳实践
```tsx
// 路由级懒加载 + 过渡状态
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={
<Dashboard />
} />
</Routes>
</Suspense>
```
> [!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