REACT系列 3、性能优化

总结:

  1. 避免不必要的重新渲染:使用 React.memo、useMemo、useCallback
  2. 合理拆分组件:保持组件小而专注
  3. 优化状态结构:避免深层嵌套,使用扁平化状态
  4. 合理使用 Context:避免在 Context 中存储频繁变化的数据
  5. 代码分割:按需加载,减少初始包大小
  6. 列表优化:使用虚拟滚动,正确的 key
  7. 及时清理:避免内存泄漏
  8. 性能监控:持续监控和优化

一、组件渲染优化

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 主要用于两个场景:

  1. 与 React.lazy 配合实现代码拆分。
  2. 与支持 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 中返回清理函数,清除定时器、取消网络请求等,避免内存泄漏。
相关推荐
爱学习的马喽4 小时前
React钩子函数完全指南:从useState到useEffect的实战详解与场景剖析
前端·javascript·react.js
JamSlade4 小时前
SSO登录验证设计要点细节(以微软 Microsoft SSO为例) 基于react python
python·react.js·microsoft
全马必破三6 小时前
React的设计理念与核心特性
前端·react.js·前端框架
洞窝技术6 小时前
前端人必看的 node_modules 瘦身秘籍:从臃肿到轻盈,Umi 项目依赖优化实战
前端·vue.js·react.js
Asort6 小时前
React函数组件深度解析:从基础到最佳实践
前端·javascript·react.js
青咕咕7 小时前
REACT系列:1、React 中 render() 的目的
react.js
mingupup9 小时前
React(二):构建一个简单的聊天助手学到的React知识
react.js
浮游本尊9 小时前
React 18.x 学习计划 - 第六天:React路由和导航
前端·学习·react.js
前端架构师-老李18 小时前
React 中 useCallback 的基本使用和原理解析
前端·react.js·前端框架