--- 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 ( ); }, (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 ; } ``` > [!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 元素来大幅降低内存占用: ```tsx // 方案1:tanstack/virtual(推荐) import { useVirtualizer } from "@tanstack/react-virtual"; function VirtualList({ items }: { items: string[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // 预估每项高度 overscan: 5, // 视口上下各多渲染 5 项 }); return (
{virtualizer.getVirtualItems().map(virtualRow => (
{items[virtualRow.index]}
))}
); } ``` > [!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 ( }> } /> } /> ); } ``` > [!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: () =>

正在加载编辑器...

, 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 ; } ``` ### 与 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