总结:
- 避免不必要的重新渲染:使用 React.memo、useMemo、useCallback
- 合理拆分组件:保持组件小而专注
- 优化状态结构:避免深层嵌套,使用扁平化状态
- 合理使用 Context:避免在 Context 中存储频繁变化的数据
- 代码分割:按需加载,减少初始包大小
- 列表优化:使用虚拟滚动,正确的 key
- 及时清理:避免内存泄漏
- 性能监控:持续监控和优化
一、组件渲染优化
1、React.memo - 防止不必要的重新渲染
- 对于函数组件,使用
React.memo可以避免不必要的重新渲染,它会对组件的 props 进行浅比较,如果 props 没有变化,则不会重新渲染。
javascript
// 普通函数组件 - 每次父组件更新都会重新渲染
const UserProfile = ({ user, onUpdate }) => {
console.log('UserProfile rendered');
return <div>{user.name}</div>;
};
// 使用 React.memo 优化 - 只有 props 变化时才重新渲染
const OptimizedUserProfile = React.memo(({ user, onUpdate }) => {
console.log('OptimizedUserProfile rendered');
return <div>{user.name}</div>;
});
// 自定义比较函数
const UserProfileWithCustomCompare = React.memo(
({ user, onUpdate }) => {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 只有当 user.id 变化时才重新渲染
return prevProps.user.id === nextProps.user.id;
}
);
2、useMemo - 缓存计算结果
可以缓存计算结果,避免在每次渲染时都进行重复计算。
javascript
function ExpensiveComponent({ items, filter }) {
// ❌ 每次渲染都会重新计算
// const filteredItems = items.filter(item => item.category === filter);
// ✅ 使用 useMemo 缓存计算结果
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => item.category === filter);
}, [items, filter]); // 依赖项变化时才重新计算
return (
<div>
{filteredItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
3、useCallback - 缓存函数
可以缓存函数,避免因为函数引用变化导致的子组件重新渲染。
ini
function UserList() {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
// ❌ 每次渲染都会创建新函数,导致子组件不必要的重渲染
// const handleDelete = (userId) => {
// setUsers(users.filter(user => user.id !== userId));
// };
// ✅ 使用 useCallback 缓存函数
const handleDelete = useCallback((userId) => {
setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
}, []); // 依赖项为空,函数只创建一次
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{filteredUsers.map(user => (
<UserItem
key={user.id}
user={user}
onDelete={handleDelete} // 现在这个引用是稳定的
/>
))}
</div>
);
}
const UserItem = React.memo(({ user, onDelete }) => {
return (
<div>
{user.name}
<button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
});
二、状态管理优化
1、使用 useReducer 管理复杂状态
- 当状态逻辑复杂,或者下一个状态依赖于前一个状态时,使用
useReducer可能比useState更合适,并且可以避免频繁传递回调函数。
javascript
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
- 当使用 Context 时,注意 Context 的值变化会导致所有消费该 Context 的组件重新渲染。可以通过将 Context 的值用
useMemo缓存,或者拆分 Context 来避免不必要的渲染。
三、代码分割与懒加载
1、 React.lazy 和 Suspense
Suspense 是 React 中用于处理异步操作(如数据获取、代码拆分)的组件,它允许你在组件等待异步操作时显示一个回退界面。目前,Suspense 主要用于两个场景:
- 与 React.lazy 配合实现代码拆分。
- 与支持 Suspense 的数据获取库(如 Relay、Next.js 或 SWR)配合使用。
注意:目前 React 官方还不支持在 Suspense 中获取数据(即尚未正式推出用于数据获取的 Suspense),但你可以使用 React.lazy 进行代码拆分,或者使用支持 Suspense 的数据获取库。
javascript
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const AnotherLazyComponent = lazy(() => import('./AnotherLazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<LazyComponent />
<AnotherLazyComponent />
</section>
</Suspense>
</div>
);
}
2、路由级别的代码分割
javascript
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Contact = lazy(() => import('./routes/Contact'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}
四、列表渲染优化
1、正确的 key 使用
避免使用索引,要使用唯一的标识
javascript
// 错误的做法:使用索引作为 key
function ItemList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> // 避免使用索引
))}
</ul>
);
}
// 正确的做法:使用唯一 ID
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // 使用唯一标识
))}
</ul>
);
}
2、虚拟滚动(对于大数据列表)
对于长列表,使用虚拟滚动技术,只渲染可见区域的内容,可以大幅提升性能。
javascript
import { FixedSizeList as List } from 'react-window';
const BigList = ({ items }) => (
<List
height={400}
itemCount={items.length}
itemSize={50}
itemData={items}
>
{({ index, style, data }) => (
<div style={style}>
{data[index].name}
</div>
)}
</List>
);
五、事件处理优化
1、防抖和节流
javascript
import { useCallback } from 'react';
import { debounce } from 'lodash';
function SearchComponent() {
const handleSearch = useCallback(
debounce((searchTerm) => {
// 执行搜索
console.log('Searching:', searchTerm);
}, 300),
[]
);
return (
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
);
}
2、事件委托
是一种利用事件冒泡机制的技术,将事件处理程序绑定在父元素上,而不是每个子元素上,通过事件目标(event.target)来识别实际触发事件的元素。 事件委托的优点:
- 减少事件处理程序的数量,节省内存。
- 对于动态添加的子元素,无需再绑定事件。
事件委托的缺点:
- 事件处理程序必须检查事件源,这增加了代码的复杂度。
- 如果事件冒泡被停止(event.stopPropagation()),则委托失效。
使用场景:当有大量类似元素需要绑定事件时,或者动态生成元素需要事件绑定时,事件委托是非常有用的。
ini
function ListComponent({ items, onItemClick }) {
const handleClick = useCallback((e) => {
if (e.target.tagName === 'LI') {
const id = e.target.dataset.id;
onItemClick(id);
}
}, [onItemClick]);
return (
<ul onClick={handleClick}>
{items.map(item => (
<li key={item.id} data-id={item.id}>
{item.name}
</li>
))}
</ul>
);
}
六、内存泄漏预防
1、清理副作用
- 在
useEffect中返回清理函数,清除定时器、取消网络请求等,避免内存泄漏。