This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/5. 工程实践篇/15-性能优化.md
T
2026-04-29 23:36:32 +08:00

352 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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