React 性能优化实践

实际项目中常用的 React 性能优化方法

问题

在实际工作中,你对 React 做过哪些优化?

解答

1. 使用 React.memo 避免不必要的重渲染

// 子组件用 React.memo 包裹,只有 props 变化时才重渲染
const UserCard = React.memo(function UserCard({ name, avatar }) {
  console.log('UserCard render');
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <span>{name}</span>
    </div>
  );
});

// 父组件
function UserList() {
  const [count, setCount] = useState(0);
  const user = { name: 'John', avatar: '/avatar.png' };

  return (
    <div>
      {/* count 变化不会导致 UserCard 重渲染 */}
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <UserCard name={user.name} avatar={user.avatar} />
    </div>
  );
}

2. useMemo 缓存计算结果

function ProductList({ products, filter }) {
  // 只有 products 或 filter 变化时才重新计算
  const filteredProducts = useMemo(() => {
    console.log('filtering products...');
    return products.filter(p => p.category === filter);
  }, [products, filter]);

  return (
    <ul>
      {filteredProducts.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

3. useCallback 缓存函数引用

function SearchBox({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  // 缓存函数,避免子组件不必要的重渲染
  const handleSearch = useCallback(() => {
    onSearch(keyword);
  }, [keyword, onSearch]);

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <SearchButton onClick={handleSearch} />
    </div>
  );
}

const SearchButton = React.memo(({ onClick }) => {
  console.log('SearchButton render');
  return <button onClick={onClick}>搜索</button>;
});

4. 虚拟列表处理大数据

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  // 只渲染可视区域内的元素
  const Row = ({ index, style }) => (
    <div style={style} className="x7o55">
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={400}        // 容器高度
      width="100%"
      itemCount={items.length}
      itemSize={50}       // 每项高度
    >
      {Row}
    </FixedSizeList>
  );
}

5. 路由懒加载

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// 懒加载组件,按需加载
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

6. 状态下沉,减少渲染范围

// ❌ 不好:整个组件都会因为 hover 状态重渲染
function ProductPage() {
  const [isHovered, setIsHovered] = useState(false);
  
  return (
    <div>
      <ExpensiveHeader />
      <div 
        onMouseEnter={() => setIsHovered(true)}
        onMouseLeave={() => setIsHovered(false)}
        style={{ background: isHovered ? '#eee' : '#fff' }}
      >
        Hover me
      </div>
      <ExpensiveFooter />
    </div>
  );
}

// ✅ 好:把状态下沉到需要的组件
function ProductPage() {
  return (
    <div>
      <ExpensiveHeader />
      <HoverBox />
      <ExpensiveFooter />
    </div>
  );
}

function HoverBox() {
  const [isHovered, setIsHovered] = useState(false);
  return (
    <div 
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{ background: isHovered ? '#eee' : '#fff' }}
    >
      Hover me
    </div>
  );
}

7. 正确使用 key

// ❌ 不好:用 index 作为 key,列表变化时会导致不必要的重渲染
{items.map((item, index) => (
  <ListItem key={index} data={item} />
))}

// ✅ 好:用唯一标识作为 key
{items.map(item => (
  <ListItem key={item.id} data={item} />
))}

关键点

  • React.memo 包裹子组件,配合 useCallback 缓存传递的函数,避免子组件不必要的重渲染
  • useMemo 缓存复杂计算结果,避免每次渲染都重新计算
  • 大列表使用 react-windowreact-virtualized 实现虚拟滚动
  • 路由和大组件使用 lazy + Suspense 实现按需加载
  • 状态下沉:把频繁变化的状态放到最小的组件中,减少重渲染范围