🎯 前言:为什么 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>
  );
}

问题:

  1. 在开头添加新项时,所有项的 index 都改变
  2. React 认为 key 改变,销毁旧组件,创建新组件
  3. 编辑状态丢失,性能浪费

✅ 解决方案

方案 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>;
}

问题:

  1. 组件卸载时,fetch 请求仍在进行
  2. 请求完成后调用 setUser,但组件已卸载
  3. React 报错:无法在已卸载组件上执行状态更新
  4. 内存泄漏

✅ 解决方案

方案 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. 每次组件挂载都添加新的监听器
  2. 组件卸载时监听器不被移除
  3. 监听器累积,内存泄漏
  4. 多个监听器同时响应,性能下降

✅ 解决方案

方案 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 性能优化原则

  1. 测量优先 - 先测量,再优化
  2. 关键路径优先 - 优化影响用户体验的部分
  3. 渐进优化 - 逐步优化,持续改进
  4. 权衡取舍 - 考虑优化成本和收益
  5. 保持可读性 - 不要过度优化牺牲代码质量

8.3 性能优化工具

开发工具:

  • React DevTools Profiler
  • Chrome DevTools Performance
  • Lighthouse
  • Webpack Bundle Analyzer

生产监控:

  • Sentry Performance
  • LogRocket
  • DataDog RUM
  • Google Analytics

🎉 总结

React 性能优化是一个持续的过程,需要:

  1. 理解原理 - 了解 React 的工作机制
  2. 识别问题 - 使用工具找出性能瓶颈
  3. 应用方案 - 选择合适的优化策略
  4. 测量效果 - 验证优化效果
  5. 持续改进 - 不断优化和监控

记住:过早优化是万恶之源,但忽视性能也是不负责任的


📚 参考资料


作者: zayfEn
发布日期: 2026年2月26日
标签: React, 性能优化, 前端开发


💡 提示: 性能优化不是一次性的工作,而是持续的过程。建立性能监控机制,定期检查和优化。

Happy Optimizing! ⚡✨