🎯 前言:为什么 React 性能优化如此重要?
在现代 Web 应用中,性能直接影响用户体验和业务指标:
- 3 秒定律:53% 的用户会放弃加载时间超过 3 秒的网站
- Google 排名:页面速度是搜索排名的重要因素
- 转化率:每延迟 1 秒,转化率下降 7%
React 虽然提供了高效的虚拟 DOM 机制,但不当的使用方式仍会导致严重的性能问题。
📊 第一部分:渲染性能优化
1.1 不必要的重渲染
📍 影响范围
问题表现:
- 父组件更新时,所有子组件都会重新渲染
- 即使子组件的 props 没有变化
- 大型组件树导致渲染成本指数级增长
性能影响:
- CPU 占用率高
- 页面卡顿
- 用户交互延迟
🔍 原因分析
React 的默认行为是:当父组件重新渲染时,所有子组件都会重新渲染,无论 props 是否改变。
// 问题代码:每次父组件更新,Child 都会重新渲染
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child name="固定值" /> {/* name 没变,但 Child 仍会重新渲染 */}
</div>
);
}
function Child({ name }) {
console.log('Child 渲染了'); // 每次都会打印
return <div>{name}</div>;
}
✅ 解决方案
方案 1:使用 React.memo
import React, { memo } from 'react';
// 使用 memo 包装组件
const Child = memo(function Child({ name }) {
console.log('Child 渲染了');
return <div>{name}</div>;
});
// 现在只有当 name 改变时,Child 才会重新渲染
方案 2:自定义比较函数
const Child = memo(
function Child({ user }) {
console.log('Child 渲染了');
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 自定义比较逻辑
return prevProps.user.id === nextProps.user.id;
}
);
📈 性能对比
// 性能测试组件
function PerformanceTest() {
const [count, setCount] = useState(0);
const items = Array.from({ length: 1000 }, (_, i) => i);
return (
<div>
<button onClick={() => setCount(count + 1)}>
触发重渲染
</button>
{/* 不优化:1000 次渲染 */}
{items.map(item => (
<SlowComponent key={item} value={item} />
))}
{/* 优化后:0 次渲染 */}
{items.map(item => (
<MemoizedSlowComponent key={item} value={item} />
))}
</div>
);
}
1.2 内联函数导致的重渲染
📍 影响范围
问题表现:
- 每次渲染都创建新的函数实例
- 导致子组件认为 props 改变
- React.memo 失效
🔍 原因分析
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* 每次渲染都创建新的 onClick 函数 */}
<Child onClick={() => console.log('clicked')} />
</div>
);
}
const Child = memo(function Child({ onClick }) {
console.log('Child 渲染了'); // 每次都会打印!
return <button onClick={onClick}>Click me</button>;
});
为什么 memo 失效?
() => console.log('clicked')每次都是新的函数- 新函数 ≠ 旧函数(引用不同)
- React.memo 认为 props 改变了
✅ 解决方案
方案 1:使用 useCallback
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 缓存函数,只有依赖改变时才创建新函数
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 空依赖数组,函数永远不会改变
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} /> {/* 传递稳定的函数引用 */}
</div>
);
}
方案 2:提取到组件外部
// 如果函数不依赖组件状态,可以提取到外部
const handleClick = () => {
console.log('clicked');
};
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
);
}
方案 3:使用状态提升
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child>
{(onClick) => (
<button onClick={() => console.log('clicked')}>
Click me
</button>
)}
</Child>
</div>
);
}
1.3 对象和数组引用问题
📍 影响范围
问题表现:
- 每次渲染都创建新的对象/数组
- 子组件认为 props 改变
- 性能优化失效
🔍 原因分析
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* 每次渲染都创建新对象 */}
<Child
style={{ color: 'red' }} // 新对象
options={{ a: 1, b: 2 }} // 新对象
items={[1, 2, 3]} // 新数组
/>
</div>
);
}
const Child = memo(function Child({ style, options, items }) {
console.log('Child 渲染了'); // 每次都会打印!
return <div style={style}>Items: {items.length}</div>;
});
✅ 解决方案
方案 1:使用 useMemo
import { useMemo } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 缓存对象和数组
const style = useMemo(() => ({ color: 'red' }), []);
const options = useMemo(() => ({ a: 1, b: 2 }), []);
const items = useMemo(() => [1, 2, 3], []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child style={style} options={options} items={items} />
</div>
);
}
方案 2:提升到组件外部
// 常量可以提取到组件外部
const DEFAULT_STYLE = { color: 'red' };
const DEFAULT_OPTIONS = { a: 1, b: 2 };
const DEFAULT_ITEMS = [1, 2, 3];
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child
style={DEFAULT_STYLE}
options={DEFAULT_OPTIONS}
items={DEFAULT_ITEMS}
/>
</div>
);
}
方案 3:使用状态管理
function Parent() {
const [count, setCount] = useState(0);
const [style] = useState({ color: 'red' });
const [options] = useState({ a: 1, b: 2 });
const [items] = useState([1, 2, 3]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child style={style} options={options} items={items} />
</div>
);
}
🔄 第二部分:状态管理优化
2.1 状态更新导致的大范围重渲染
📍 影响范围
问题表现:
- 一个状态更新导致整个组件树重渲染
- 不相关的组件也被迫更新
- 性能随组件树规模下降
🔍 原因分析
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
return (
<div>
<UserProfile user={user} />
<UserSettings user={user} setUser={setUser} />
<UserPosts userId={user.id} />
<UserComments userId={user.id} />
{/* 更新 age 时,所有子组件都会重新渲染 */}
<button onClick={() => setUser({ ...user, age: user.age + 1 })}>
增加年龄
</button>
</div>
);
}
✅ 解决方案
方案 1:状态拆分
function App() {
// 将状态拆分成更细粒度
const [userName, setUserName] = useState('Alice');
const [userAge, setUserAge] = useState(25);
const [userId] = useState(123);
return (
<div>
<UserProfile name={userName} age={userAge} />
<UserSettings
name={userName}
age={userAge}
setName={setUserName}
setAge={setUserAge}
/>
<UserPosts userId={userId} />
<UserComments userId={userId} />
<button onClick={() => setUserAge(userAge + 1)}>
增加年龄
</button>
</div>
);
}
方案 2:状态下沉
function App() {
const [userId] = useState(123);
return (
<div>
<UserProfile userId={userId} />
<UserSettings userId={userId} />
<UserPosts userId={userId} />
<UserComments userId={userId} />
</div>
);
}
function UserProfile({ userId }) {
// 状态管理下移到需要的组件
const [user, setUser] = useUser(userId);
return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
<button onClick={() => setUser({ ...user, age: user.age + 1 })}>
增加年龄
</button>
</div>
);
}
方案 3:使用 Context 分割
// 分离不同的 Context
const UserContext = createContext();
const UserActionsContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={{ setUser }}>
<AppContent />
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
// 只订阅需要的数据
function UserProfile() {
const user = useContext(UserContext); // 只订阅 user 数据
return <h1>{user.name}</h1>;
}
function UserSettings() {
const { setUser } = useContext(UserActionsContext); // 只订阅 actions
return <button onClick={() => setUser(/* ... */)}>更新</button>;
}
2.2 Context 性能问题
📍 影响范围
问题表现:
- Context 值改变时,所有消费者都重新渲染
- 即使消费者只使用 Context 的部分数据
- 大型应用中 Context 成为性能瓶颈
🔍 原因分析
const AppContext = createContext();
function App() {
const [state, setState] = useState({
user: { name: 'Alice', age: 25 },
theme: 'light',
language: 'zh-CN',
notifications: []
});
return (
<AppContext.Provider value={state}>
<UserProfile /> {/* 只需要 user */}
<ThemeToggle /> {/* 只需要 theme */}
<LanguageSelector /> {/* 只需要 language */}
<Notifications /> {/* 只需要 notifications */}
</AppContext.Provider>
);
}
function UserProfile() {
const { user } = useContext(AppContext);
// 当 theme、language、notifications 改变时,这个组件也会重新渲染!
console.log('UserProfile 渲染了');
return <div>{user.name}</div>;
}
✅ 解决方案
方案 1:拆分 Context
const UserContext = createContext();
const ThemeContext = createContext();
const LanguageContext = createContext();
const NotificationsContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('zh-CN');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
<NotificationsContext.Provider value={{ notifications, setNotifications }}>
<AppContent />
</NotificationsContext.Provider>
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function UserProfile() {
const { user } = useContext(UserContext); // 只订阅 user
// 现在只有 user 改变时才会重新渲染
return <div>{user.name}</div>;
}
方案 2:使用选择器模式
import { createContext, useContext, useMemo } from 'react';
const AppContext = createContext();
// 自定义 hook:只订阅特定字段
function useAppContext(selector) {
const context = useContext(AppContext);
return useMemo(() => selector(context), [context, selector]);
}
function App() {
const [state, setState] = useState({
user: { name: 'Alice', age: 25 },
theme: 'light',
language: 'zh-CN'
});
return (
<AppContext.Provider value={state}>
<AppContent />
</AppContext.Provider>
);
}
function UserProfile() {
// 只选择需要的字段
const userName = useAppContext(state => state.user.name);
return <div>{userName}</div>;
}
方案 3:使用第三方库(如 Zustand、Jotai)
import { create } from 'zustand';
// 使用 Zustand 进行状态管理
const useStore = create((set) => ({
user: { name: 'Alice', age: 25 },
theme: 'light',
language: 'zh-CN',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
}));
function UserProfile() {
// 只订阅 user,其他状态改变不会触发重渲染
const user = useStore(state => state.user);
return <div>{user.name}</div>;
}
function ThemeToggle() {
// 只订阅 theme
const theme = useStore(state => state.theme);
const setTheme = useStore(state => state.setTheme);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
2.3 状态更新批处理
📍 影响范围
问题表现:
- 多次状态更新导致多次渲染
- 同步代码中的多个 setState 触发多次渲染
- 性能浪费
🔍 原因分析
function Component() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const [count3, setCount3] = useState(0);
const handleClick = () => {
// 在 React 17 及以下,这会导致 3 次渲染
setCount1(count1 + 1);
setCount2(count2 + 1);
setCount3(count3 + 1);
};
console.log('组件渲染了'); // 打印 3 次
return (
<div>
<button onClick={handleClick}>更新所有状态</button>
<p>{count1} {count2} {count3}</p>
</div>
);
}
✅ 解决方案
方案 1:React 18 自动批处理
// React 18 中,所有更新都会自动批处理
function Component() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const [count3, setCount3] = useState(0);
const handleClick = () => {
// React 18:只触发 1 次渲染
setCount1(count1 + 1);
setCount2(count2 + 1);
setCount3(count3 + 1);
};
console.log('组件渲染了'); // 只打印 1 次
return (
<div>
<button onClick={handleClick}>更新所有状态</button>
<p>{count1} {count2} {count3}</p>
</div>
);
}
方案 2:使用 unstable_batchedUpdates(React 17)
import { unstable_batchedUpdates } from 'react-dom';
function Component() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClick = () => {
// 手动批处理
unstable_batchedUpdates(() => {
setCount1(count1 + 1);
setCount2(count2 + 1);
});
};
return (
<div>
<button onClick={handleClick}>更新所有状态</button>
</div>
);
}
方案 3:合并状态
function Component() {
const [state, setState] = useState({
count1: 0,
count2: 0,
count3: 0
});
const handleClick = () => {
// 一次更新,一次渲染
setState(prev => ({
count1: prev.count1 + 1,
count2: prev.count2 + 1,
count3: prev.count3 + 1
}));
};
return (
<div>
<button onClick={handleClick}>更新所有状态</button>
<p>{state.count1} {state.count2} {state.count3}</p>
</div>
);
}
方案 4:使用 useReducer
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment_all':
return {
count1: state.count1 + 1,
count2: state.count2 + 1,
count3: state.count3 + 1
};
default:
return state;
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, {
count1: 0,
count2: 0,
count3: 0
});
const handleClick = () => {
// 一次 dispatch,一次渲染
dispatch({ type: 'increment_all' });
};
return (
<div>
<button onClick={handleClick}>更新所有状态</button>
<p>{state.count1} {state.count2} {state.count3}</p>
</div>
);
}
📦 第三部分:列表和大数据优化
3.1 长列表性能问题
📍 影响范围
问题表现:
- 渲染成千上万条数据时页面卡顿
- 滚动性能差
- 内存占用高
- 首次加载时间长
🔍 原因分析
function ItemList({ items }) {
return (
<div>
{items.map(item => (
<Item key={item.id} item={item} /> // 10000 个组件同时渲染
))}
</div>
);
}
function App() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
return <ItemList items={items} />; // 严重性能问题
}
性能影响:
- 渲染 10000 个组件需要几秒
- 滚动时每秒渲染 60 次 × 10000 = 600000 次操作
- DOM 节点过多导致浏览器重排重绘开销大
✅ 解决方案
方案 1:虚拟滚动(react-window)
import { FixedSizeList as List } from 'react-window';
function ItemList({ items }) {
// 每一行的渲染函数
const Row = ({ index, style }) => (
<div style={style}>
<Item item={items[index]} />
</div>
);
return (
<List
height={600} // 可视区域高度
itemCount={items.length} // 总条目数
itemSize={50} // 每行高度
width="100%" // 宽度
>
{Row}
</List>
);
}
function App() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
return <ItemList items={items} />; // 流畅渲染
}
方案 2:分页加载
function ItemList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
setLoading(true);
const newItems = await fetchItems(page);
setItems([...items, ...newItems]);
setPage(page + 1);
setLoading(false);
};
return (
<div>
{items.map(item => (
<Item key={item.id} item={item} />
))}
{loading ? (
<div>加载中...</div>
) : (
<button onClick={loadMore}>加载更多</button>
)}
</div>
);
}
方案 3:无限滚动
import { useEffect, useRef } from 'react';
function ItemList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const loaderRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => observer.disconnect();
}, [page]);
const loadMore = async () => {
const newItems = await fetchItems(page);
setItems([...items, ...newItems]);
setPage(page + 1);
};
return (
<div>
{items.map(item => (
<Item key={item.id} item={item} />
))}
<div ref={loaderRef}>加载中...</div>
</div>
);
}
3.2 列表项的 key 问题
📍 影响范围
问题表现:
- 使用 index 作为 key 导致状态错乱
- 列表更新性能差
- 动画和过渡效果异常
🔍 原因分析
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Todo 1' },
{ id: 2, text: 'Todo 2' },
{ id: 3, text: 'Todo 3' }
]);
const addTodo = () => {
const newTodo = { id: Date.now(), text: `Todo ${todos.length + 1}` };
setTodos([newTodo, ...todos]); // 在开头添加
};
return (
<div>
<button onClick={addTodo}>添加 Todo</button>
{todos.map((todo, index) => (
<TodoItem
key={index} // ❌ 使用 index 作为 key
todo={todo}
/>
))}
</div>
);
}
function TodoItem({ todo }) {
const [isEditing, setIsEditing] = useState(false);
return (
<div>
{isEditing ? (
<input defaultValue={todo.text} />
) : (
<span>{todo.text}</span>
)}
<button onClick={() => setIsEditing(!isEditing)}>
编辑
</button>
</div>
);
}
问题:
- 在开头添加新项时,所有项的 index 都改变
- React 认为 key 改变,销毁旧组件,创建新组件
- 编辑状态丢失,性能浪费
✅ 解决方案
方案 1:使用唯一 ID
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Todo 1' },
{ id: 2, text: 'Todo 2' },
{ id: 3, text: 'Todo 3' }
]);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id} // ✅ 使用唯一 ID
todo={todo}
/>
))}
</div>
);
}
方案 2:生成唯一 ID
import { v4 as uuidv4 } from 'uuid';
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
const newTodo = {
id: uuidv4(), // 生成唯一 ID
text
};
setTodos([newTodo, ...todos]);
};
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
方案 3:使用短 ID
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
const newTodo = {
id: generateId(),
text
};
setTodos([newTodo, ...todos]);
};
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
🧹 第四部分:内存泄漏和副作用优化
4.1 未清理的副作用
📍 影响范围
问题表现:
- 组件卸载后仍然执行副作用
- 内存占用持续增长
- 控制台报错(在已卸载组件上调用 setState)
🔍 原因分析
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data); // 如果组件已卸载,这里会报错
});
}, [userId]);
// ❌ 没有清理函数
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
问题:
- 组件卸载时,fetch 请求仍在进行
- 请求完成后调用 setUser,但组件已卸载
- React 报错:无法在已卸载组件上执行状态更新
- 内存泄漏
✅ 解决方案
方案 1:使用 AbortController
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(data => {
setUser(data);
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error(error);
}
});
// 清理函数:取消请求
return () => {
controller.abort();
};
}, [userId]);
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
方案 2:使用标志位
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let isMounted = true; // 标志位
fetchUser(userId).then(data => {
if (isMounted) { // 检查组件是否仍然挂载
setUser(data);
}
});
// 清理函数
return () => {
isMounted = false;
};
}, [userId]);
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
方案 3:使用自定义 Hook
function useAsyncEffect(asyncFunction, deps) {
useEffect(() => {
let isMounted = true;
asyncFunction().then(result => {
if (isMounted) {
// 处理结果
}
});
return () => {
isMounted = false;
};
}, deps);
}
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useAsyncEffect(async () => {
const data = await fetchUser(userId);
setUser(data);
}, [userId]);
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
4.2 事件监听器泄漏
📍 影响范围
问题表现:
- 组件卸载后事件监听器仍然存在
- 内存占用持续增长
- 性能下降(多个监听器同时响应)
🔍 原因分析
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
// 添加滚动监听
window.addEventListener('scroll', () => {
setScrollY(window.scrollY);
});
// ❌ 没有移除监听器
}, []);
return <div>滚动位置: {scrollY}px</div>;
}
问题:
- 每次组件挂载都添加新的监听器
- 组件卸载时监听器不被移除
- 监听器累积,内存泄漏
- 多个监听器同时响应,性能下降
✅ 解决方案
方案 1:正确清理监听器
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
// 添加监听器
window.addEventListener('scroll', handleScroll);
// 清理函数:移除监听器
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return <div>滚动位置: {scrollY}px</div>;
}
方案 2:使用自定义 Hook
function useEventListener(eventName, handler, element = window) {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEventListener('scroll', () => {
setScrollY(window.scrollY);
});
return <div>滚动位置: {scrollY}px</div>;
}
4.3 定时器泄漏
📍 影响范围
问题表现:
- 组件卸载后定时器仍在运行
- 尝试更新已卸载组件的状态
- 内存泄漏
🔍 原因分析
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 设置定时器
setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// ❌ 没有清理定时器
}, []);
return <div>计时: {seconds}秒</div>;
}
✅ 解决方案
方案 1:清理定时器
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 清理函数:清除定时器
return () => {
clearInterval(intervalId);
};
}, []);
return <div>计时: {seconds}秒</div>;
}
方案 2:使用自定义 Hook
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Timer() {
const [seconds, setSeconds] = useState(0);
useInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return <div>计时: {seconds}秒</div>;
}
🌐 第五部分:网络请求优化
5.1 重复请求
📍 影响范围
问题表现:
- 同一数据被多次请求
- 网络带宽浪费
- 服务器压力增大
🔍 原因分析
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
function App() {
const [userId, setUserId] = useState(1);
return (
<div>
<UserProfile userId={userId} />
<UserProfile userId={userId} /> {/* 重复请求 */}
<UserProfile userId={userId} /> {/* 重复请求 */}
</div>
);
}
✅ 解决方案
方案 1:使用 React Query
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery(
['user', userId],
() => fetchUser(userId),
{
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
cacheTime: 10 * 60 * 1000, // 缓存 10 分钟
}
);
if (isLoading) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
function App() {
const [userId, setUserId] = useState(1);
return (
<div>
<UserProfile userId={userId} />
<UserProfile userId={userId} /> {/* 从缓存读取 */}
<UserProfile userId={userId} /> {/* 从缓存读取 */}
</div>
);
}
方案 2:使用 SWR
import useSWR from 'swr';
function UserProfile({ userId }) {
const { data: user, error } = useSWR(
`/api/users/${userId}`,
fetcher,
{
revalidateOnFocus: false, // 窗口聚焦时不重新验证
dedupingInterval: 5000, // 5 秒内去重
}
);
if (error) return <div>加载失败</div>;
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
方案 3:手动缓存
const cache = new Map();
function useCachedFetch(key, fetcher) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 检查缓存
if (cache.has(key)) {
setData(cache.get(key));
return;
}
setLoading(true);
fetcher().then(result => {
cache.set(key, result);
setData(result);
setLoading(false);
});
}, [key]);
return { data, loading };
}
function UserProfile({ userId }) {
const { data: user, loading } = useCachedFetch(
`user-${userId}`,
() => fetchUser(userId)
);
if (loading) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
5.2 请求瀑布流
📍 影响范围
问题表现:
- 多个请求串行执行
- 总加载时间等于所有请求时间之和
- 用户体验差
🔍 原因分析
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
const [comments, setComments] = useState(null);
useEffect(() => {
// 串行请求:总时间 = t1 + t2 + t3
fetchUser(userId).then(user => {
setUser(user);
fetchPosts(user.id).then(posts => {
setPosts(posts);
fetchComments(posts[0].id).then(comments => {
setComments(comments);
});
});
});
}, [userId]);
return (
<div>
<h1>{user?.name}</h1>
<p>Posts: {posts?.length}</p>
<p>Comments: {comments?.length}</p>
</div>
);
}
✅ 解决方案
方案 1:并行请求
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
const [comments, setComments] = useState(null);
useEffect(() => {
// 并行请求:总时间 = max(t1, t2, t3)
Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId)
]).then(([user, posts, comments]) => {
setUser(user);
setPosts(posts);
setComments(comments);
});
}, [userId]);
return (
<div>
<h1>{user?.name}</h1>
<p>Posts: {posts?.length}</p>
<p>Comments: {comments?.length}</p>
</div>
);
}
方案 2:使用 React Query 并行查询
import { useQueries } from '@tanstack/react-query';
function UserProfile({ userId }) {
const queries = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
},
{
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
},
{
queryKey: ['comments', userId],
queryFn: () => fetchComments(userId),
},
],
});
const [userQuery, postsQuery, commentsQuery] = queries;
if (userQuery.isLoading) return <div>加载中...</div>;
return (
<div>
<h1>{userQuery.data.name}</h1>
<p>Posts: {postsQuery.data?.length}</p>
<p>Comments: {commentsQuery.data?.length}</p>
</div>
);
}
方案 3:使用 Suspense
import { Suspense } from 'react';
// 数据获取函数
function fetchUser(userId) {
// 返回一个可以 throw promise 的资源
throw new Promise(/* ... */);
}
function UserProfile({ userId }) {
const user = fetchUser(userId); // 会 suspend
return <h1>{user.name}</h1>;
}
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<UserProfile userId={1} />
</Suspense>
);
}
📦 第六部分:Bundle 优化
6.1 代码分割
📍 影响范围
问题表现:
- 首次加载时间过长
- 用户下载了很多当前不需要的代码
- 带宽浪费
🔍 原因分析
// 所有代码打包在一起
import { HeavyComponent } from './HeavyComponent';
import { AnotherHeavyComponent } from './AnotherHeavyComponent';
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
显示重型组件
</button>
{showHeavy && (
<>
<HeavyComponent />
<AnotherHeavyComponent />
</>
)}
</div>
);
}
问题:
- 用户可能永远不会点击按钮
- 但仍然下载了 HeavyComponent 的代码
- 首次加载时间变长
✅ 解决方案
方案 1:动态导入
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AnotherHeavyComponent = lazy(() => import('./AnotherHeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
显示重型组件
</button>
{showHeavy && (
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
<AnotherHeavyComponent />
</Suspense>
)}
</div>
);
}
方案 2:路由级代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 路由级懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
方案 3:条件加载
function App() {
const [showMap, setShowMap] = useState(false);
const [MapComponent, setMapComponent] = useState(null);
const loadMap = async () => {
const module = await import('./HeavyMapComponent');
setMapComponent(() => module.default);
setShowMap(true);
};
return (
<div>
<button onClick={loadMap}>
显示地图
</button>
{showMap && MapComponent && <MapComponent />}
</div>
);
}
6.2 第三方库优化
📍 影响范围
问题表现:
- 导入整个库但只使用一小部分功能
- Bundle 体积过大
- 加载时间过长
🔍 原因分析
// 导入整个 lodash
import _ from 'lodash'; // ~70KB
function App() {
const result = _.map([1, 2, 3], n => n * 2);
return <div>{result}</div>;
}
// 导入整个 moment.js
import moment from 'moment'; // ~70KB
function DateDisplay() {
return <div>{moment().format('YYYY-MM-DD')}</div>;
}
✅ 解决方案
方案 1:按需导入
// 只导入需要的函数
import map from 'lodash/map'; // ~2KB
function App() {
const result = map([1, 2, 3], n => n * 2);
return <div>{result}</div>;
}
// 使用更轻量的日期库
import { format } from 'date-fns'; // ~2KB
function DateDisplay() {
return <div>{format(new Date(), 'yyyy-MM-dd')}</div>;
}
方案 2:使用 Tree Shaking 友好的库
// 使用 ES6 模块
import { map, filter, find } from 'lodash-es';
function App() {
const result = map([1, 2, 3], n => n * 2);
return <div>{result}</div>;
}
方案 3:替换重型库
// 替换 moment.js
// 之前
import moment from 'moment'; // ~70KB
const date = moment().format('YYYY-MM-DD');
// 之后
import { format } from 'date-fns'; // ~2KB
const date = format(new Date(), 'yyyy-MM-dd');
// 替换 lodash
// 之前
import _ from 'lodash'; // ~70KB
// 之后
import { map, filter } from 'lodash-es'; // 按需导入
// 或者使用原生方法
const result = [1, 2, 3].map(n => n * 2);
📊 第七部分:性能监控和测量
7.1 React DevTools Profiler
📍 使用场景
适用于:
- 识别性能瓶颈
- 测量组件渲染时间
- 分析渲染原因
🔍 基本使用
import { Profiler } from 'react';
function onRenderCallback(
id, // Profiler 的 id
phase, // "mount" 或 "update"
actualDuration, // 本次渲染花费的时间
baseDuration, // 不使用 memo 时预计花费的时间
startTime, // 开始渲染的时间戳
commitTime // 提交的时间戳
) {
console.log({
id,
phase,
actualDuration,
baseDuration
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<ComponentTree />
</Profiler>
);
}
✅ 性能分析
function PerformanceAnalysis() {
return (
<div>
<Profiler id="Header" onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} 花费 ${actualDuration}ms`);
}}>
<Header />
</Profiler>
<Profiler id="Content" onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} 花费 ${actualDuration}ms`);
}}>
<Content />
</Profiler>
<Profiler id="Footer" onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} 花费 ${actualDuration}ms`);
}}>
<Footer />
</Profiler>
</div>
);
}
7.2 使用 React 内置的性能追踪
📍 使用场景
适用于:
- 测量用户交互性能
- 追踪特定操作的耗时
- 性能监控和报警
🔍 基本使用
import { unstable_trace as trace } from 'scheduler/tracing';
function MyComponent() {
const handleClick = () => {
trace('按钮点击', performance.now(), () => {
// 执行一些操作
setState(newState);
});
};
return <button onClick={handleClick}>点击我</button>;
}
🎯 第八部分:最佳实践总结
8.1 性能优化检查清单
✅ 渲染优化
- 使用 React.memo 避免不必要的重渲染
- 使用 useCallback 缓存回调函数
- 使用 useMemo 缓存计算结果
- 避免内联函数和对象
✅ 状态管理
- 合理拆分状态
- 状态下沉到需要的组件
- 使用 Context 时注意性能影响
- 考虑使用状态管理库
✅ 列表优化
- 使用虚拟滚动处理长列表
- 使用稳定的 key
- 分页或无限滚动
✅ 副作用管理
- 清理所有副作用
- 取消未完成的请求
- 移除事件监听器
- 清除定时器
✅ 网络请求
- 使用数据缓存
- 并行请求
- 请求去重
- 使用 SWR 或 React Query
✅ Bundle 优化
- 代码分割
- 路由级懒加载
- 按需导入第三方库
- Tree Shaking
8.2 性能优化原则
- 测量优先 - 先测量,再优化
- 关键路径优先 - 优化影响用户体验的部分
- 渐进优化 - 逐步优化,持续改进
- 权衡取舍 - 考虑优化成本和收益
- 保持可读性 - 不要过度优化牺牲代码质量
8.3 性能优化工具
开发工具:
- React DevTools Profiler
- Chrome DevTools Performance
- Lighthouse
- Webpack Bundle Analyzer
生产监控:
- Sentry Performance
- LogRocket
- DataDog RUM
- Google Analytics
🎉 总结
React 性能优化是一个持续的过程,需要:
- 理解原理 - 了解 React 的工作机制
- 识别问题 - 使用工具找出性能瓶颈
- 应用方案 - 选择合适的优化策略
- 测量效果 - 验证优化效果
- 持续改进 - 不断优化和监控
记住:过早优化是万恶之源,但忽视性能也是不负责任的。
📚 参考资料
- React 官方文档 - 性能优化
- React 文档 - memo
- React 文档 - useCallback
- React 文档 - useMemo
- react-window 文档
- React Query 文档
- SWR 文档
作者: zayfEn
发布日期: 2026年2月26日
标签: React, 性能优化, 前端开发
💡 提示: 性能优化不是一次性的工作,而是持续的过程。建立性能监控机制,定期检查和优化。
Happy Optimizing! ⚡✨