Init
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
---
|
||||
tags: [React, Performance, Optimization, Frontend]
|
||||
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
|
||||
graph TB
|
||||
A[发现性能问题] --> B["选择诊断工具"]
|
||||
|
||||
B --> C["React DevTools Profiler"]
|
||||
B --> D["Chrome Performance Tab"]
|
||||
B --> E["Lighthouse"]
|
||||
B --> F["Web Vitals 监控"]
|
||||
|
||||
C --> G["定位重渲染的组件和原因"]
|
||||
D --> H["分析主线程阻塞时段"]
|
||||
E --> I["整体 LCP/INP/CLS 评分"]
|
||||
F --> J["生产环境真实用户数据"]
|
||||
|
||||
style C fill:#61DAFB,color:#000
|
||||
style H fill:#F5A87D,color:#000
|
||||
```
|
||||
|
||||
### React DevTools Profiler 使用要点
|
||||
|
||||
1. 录制期间进行关键交互(点击、输入)
|
||||
2. 观察 **Commit 颜色** —— 红色越深表示重渲染越多
|
||||
3. 展开组件树,关注 "why did this render" 原因
|
||||
4. 对比优化前后的 Commit 时间变化
|
||||
|
||||
## React.memo —— 阻止子组件重渲染
|
||||
|
||||
```tsx
|
||||
const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
|
||||
// props.items 引用不变时,跳过整个子树的 re-render
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.items === nextProps.items); // 自定义比较函数
|
||||
```
|
||||
|
||||
> [!warning] React.memo 的适用边界
|
||||
> - 只对**纯组件**有用——相同 props 必须产生相同的输出
|
||||
> - 父组件每次传新对象/函数作 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 元素来大幅降低内存占用:
|
||||
|
||||
```tsx
|
||||
// 方案1:tanstack/virtual(推荐)
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
|
||||
function VirtualList({ items }: { items: string[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 50, // 预估每项高度
|
||||
overscan: 5, // 视口上下各多渲染 5 项
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
|
||||
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
|
||||
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{items[virtualRow.index]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 何时需要虚拟列表?
|
||||
> - 列表项 > ~50 且每帧渲染耗时可感知
|
||||
> - 固定高度的项目比可变高度更容易实现
|
||||
> - React Window / React Virtualized 是老牌的成熟方案
|
||||
|
||||
## Code Splitting
|
||||
|
||||
Code Splitting 的核心理念:**用户不需要一次性下载所有代码**。按需加载可以显著降低首屏时间。
|
||||
|
||||
### 路由级拆分
|
||||
|
||||
```tsx
|
||||
import { lazy, Suspense } from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
// 每个路由对应一个 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>正在加载编辑器...</p>, ssr: false }
|
||||
);
|
||||
|
||||
// React 原生写法——配合 Suspense
|
||||
const MapComponent = lazy(() => import("./MapComponent"));
|
||||
// 可指定 preloadStrategy——预加载策略
|
||||
const PrefetchModal = lazy(
|
||||
() => import("./HeavyModal")
|
||||
);
|
||||
```
|
||||
|
||||
### Bundle 分析与优化
|
||||
|
||||
```bash
|
||||
# Next.js 项目
|
||||
npx next-bundle-analyzer
|
||||
|
||||
# Vite / Webpack 通用方案
|
||||
npm install --save-dev rollup-plugin-visualizer
|
||||
npm run build && npx visualizer
|
||||
```
|
||||
|
||||
> [!tip] Bundle 大小目标
|
||||
> | 层级 | 目标大小(gzipped)|
|
||||
> |------|---------------------|
|
||||
> | 首屏 chunk | < 150KB |
|
||||
> | 单个 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 体积、使用 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
|
||||
Reference in New Issue
Block a user