一、渲染优化(解决 "重复渲染、无效渲染",最核心)
React 性能问题 80% 来自不必要的组件重渲染,这是优化的首要方向:
1. 避免组件无意义重渲染
(1)基础版:React.memo + useCallback + useMemo
React.memo:缓存函数组件,仅当 props 浅对比变化时才重渲染(类组件用PureComponent/shouldComponentUpdate);useCallback:缓存函数引用,避免因函数重新创建导致子组件重渲染;useMemo:缓存计算结果 / 组件,避免重复计算 / 重复渲染。
示例代码:
jsx
javascript
import { useState, memo, useCallback, useMemo } from 'react';
// 子组件:用React.memo缓存,仅props变化时重渲染
const Child = memo(({ onClick, data }) => {
console.log('Child 重渲染');
return <button onClick={onClick}>{data.name}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [user] = useState({ name: 'React' });
// 缓存函数:避免每次渲染创建新函数,导致Child重渲染
const handleClick = useCallback(() => {
console.log('点击');
}, []); // 依赖为空,永久缓存
// 缓存复杂数据:避免每次渲染重新生成,导致Child重渲染
const memoizedData = useMemo(() => ({ ...user }), [user]);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count+1</button>
{/* 即使count变化,Child也不会重渲染 */}
<Child onClick={handleClick} data={memoizedData} />
</div>
);
}
核心说明:
- 若不用
React.memo+useCallback,点击count+1时,Parent 重渲染会创建新的handleClick函数,导致 Child 也跟着重渲染; - 仅对频繁重渲染的组件使用这些 API,普通组件无需过度封装(有微小性能开销)。
(2)进阶版:避免传递 "动态对象 / 数组" 作为 props
jsx
javascript
// 错误写法:每次渲染创建新对象,触发子组件重渲染
<Child filter={{ type: 'all', status: 1 }} />
// 正确写法:缓存动态对象
const filter = useMemo(() => ({ type: 'all', status: 1 }), []);
<Child filter={filter} />
2. 精准控制状态作用域
- 把状态放在最小必要组件中,避免状态提升过高导致无关组件重渲染;
- 示例:搜索框的输入状态只放在搜索组件内,而非页面根组件,避免整个页面因输入框变化重渲染。
3. 虚拟列表(解决长列表卡顿)
当列表数据超过 100 条时,渲染所有 DOM 会导致卡顿,用「虚拟列表」只渲染可视区域的 DOM:
- 推荐库:
react-window/react-virtualized(轻量选前者,功能全选后者)。
示例(react-window):
jsx
javascript
import { FixedSizeList as List } from 'react-window';
// 长列表数据(比如10000条)
const listData = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
function LongList() {
// 只渲染可视区域的item
const Row = ({ index, style }) => (
<div style={style}>{listData[index]}</div>
);
return (
<List
height={500} // 可视区域高度
width="100%" // 宽度
itemCount={listData.length} // 总数据量
itemSize={50} // 每个item的高度
>
{Row}
</List>
);
}
二、加载优化(解决 "首屏慢、白屏时间长")
1. 路由懒加载(分割代码,减小首屏包体积)
用React.lazy + Suspense实现路由级别的代码分割,首屏只加载当前路由的代码:
示例代码:
jsx
javascript
import { BrowserRouter, Routes, Route, Suspense } from 'react-router-dom';
// 加载中占位组件
const Loading = () => <div>加载中...</div>;
// 懒加载路由组件(分割代码)
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const User = React.lazy(() => import('./pages/User'));
function App() {
return (
<BrowserRouter>
{/* Suspense包裹懒加载组件,指定加载中UI */}
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user" element={<User />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. 资源预加载 / 预获取
- 预加载(Preload):加载当前页面必需的资源(比如关键 CSS、字体);
- 预获取(Prefetch):空闲时加载未来可能用到的资源(比如下一个路由的代码)。
示例(HTML 中配置):
html
<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/iconfont.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预获取About页面的代码包(假设打包后文件名是about.js) -->
<link rel="prefetch" href="/static/js/about.js">
3. 图片优化
- 用
img的loading="lazy"实现图片懒加载(仅滚动到可视区域才加载); - 用 WebP/AVIF 等现代图片格式,减小体积;
- 适配不同屏幕:用
srcset+sizes加载不同分辨率的图片。
示例:
jsx
javascript
<img
src="/images/photo-480w.jpg"
srcset="/images/photo-480w.jpg 480w, /images/photo-800w.jpg 800w"
sizes="(max-width: 600px) 480px, 800px"
loading="lazy"
alt="示例图片"
/>
三、打包优化(减小构建包体积,提升加载速度)
基于 Webpack/Vite(React 主流构建工具)的优化技巧:
1. 分析包体积,定位大依赖
- Webpack:用
webpack-bundle-analyzer生成包体积分析图; - Vite:用
rollup-plugin-visualizer。
使用示例(Webpack):
js
javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin() // 启动后自动打开分析页面
]
};
运行npm run build后,会看到哪些依赖体积大(比如 lodash、moment.js),针对性优化。
2. 按需引入第三方库
- 避免全量引入:比如
lodash只引入需要的方法,antd/Element UI开启按需加载。
示例:
jsx
javascript
// 错误:全量引入lodash(体积大)
import _ from 'lodash';
// 正确:只引入需要的方法
import debounce from 'lodash/debounce';
// antd按需加载(需配置babel-plugin-import)
import { Button, Table } from 'antd';
3. 替换大体积依赖
- 用轻量库替代:比如
moment.js(体积大)→dayjs/date-fns(体积小,按需引入); - 示例:
import dayjs from 'dayjs'替代import moment from 'moment'。
4. 压缩代码和资源
- Webpack:用
TerserPlugin压缩 JS,css-minimizer-webpack-plugin压缩 CSS; - 开启 Gzip/Brotli 压缩(后端 Nginx 配置,或前端构建时生成压缩包)。
四、通用优化(其他易落地的小技巧)
1. 减少 DOM 操作
- 避免频繁修改 DOM:用 React 状态驱动视图,而非直接操作 DOM;
- 长列表用 Fragment(
<>)包裹,避免多余的父节点。
2. 防抖 / 节流处理高频事件
比如搜索框输入、窗口 resize、滚动事件,避免频繁触发函数:
示例(防抖):
jsx
javascript
import { useState, useCallback } from 'react';
import debounce from 'lodash/debounce';
function Search() {
const [keyword, setKeyword] = useState('');
// 防抖:输入停止500ms后才执行搜索
const handleSearch = useCallback(
debounce((val) => {
console.log('搜索:', val);
// 发请求逻辑
}, 500),
[]
);
return (
<input
type="text"
value={keyword}
onChange={(e) => {
setKeyword(e.target.value);
handleSearch(e.target.value);
}}
placeholder="请输入搜索关键词"
/>
);
}
3. 合理使用缓存
- 接口数据缓存:用
useState+useEffect缓存请求结果,或用SWR/React Query(自动缓存、重试、失效); - 本地缓存:常用数据存
localStorage/sessionStorage,避免重复请求。
示例(SWR 缓存接口数据):
jsx
javascript
import useSWR from 'swr';
// 封装请求函数
const fetcher = (url) => fetch(url).then(res => res.json());
function UserList() {
// SWR自动缓存数据,重新进入页面无需重复请求
const { data, error } = useSWR('/api/users', fetcher);
if (error) return <div>加载失败</div>;
if (!data) return <div>加载中</div>;
return <div>{data.map(user => <p key={user.id}>{user.name}</p>)}</div>;
}
4. 避免内存泄漏
- 清理副作用:比如定时器、事件监听、请求取消,在
useEffect的返回函数中销毁;
示例:
jsx
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行');
}, 1000);
// 组件卸载时清理定时器
return () => clearInterval(timer);
}, []);
总结
- 渲染优化 是核心:优先用
React.memo+useCallback+useMemo避免无效重渲染,长列表用虚拟列表; - 加载优化提体验:路由懒加载、图片懒加载、资源预加载,减小首屏加载时间;
- 打包优化减体积:分析包体积、按需引入依赖、替换大体积库,降低资源加载耗时;
- 通用优化补细节:防抖节流、合理缓存、清理副作用,避免性能问题和内存泄漏。
优化的核心原则是「先定位问题,再针对性优化」------ 先用 Chrome DevTools 的 Performance 面板分析卡顿原因,用 Network 面板分析加载问题,再选择对应的技巧落地,避免无意义的过度优化。