返回博客列表
前端
2024年6月15日
20 分钟阅读

React 性能优化实战:从 Profiler 到代码分割的完整指南

React 性能优化实战:从 Profiler 到代码分割的完整指南

性能优化不是猜测,而是基于数据的决策。本文将带你从性能测量开始,深入探讨 React 应用的性能优化策略和实战技巧。

性能测量:React DevTools Profiler

使用 Profiler API

React 提供了 Profiler 组件来测量渲染性能:

import { Profiler } from 'react';
 
function onRenderCallback(
  id,           // 被测量的组件树 ID
  phase,        // "mount" 或 "update"
  actualDuration, // 本次渲染花费的时间(ms)
  baseDuration,   // 不使用 memo 时估算的渲染时间
  startTime,      // React 开始渲染的时间戳
  commitTime      // React 提交更新的时间戳
) {
  console.log('Render:', {
    id,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime
  });
}
 
function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <ExpensiveComponent />
    </Profiler>
  );
}

生产环境性能监控

// 创建性能监控 Hook
function usePerformanceMonitor(componentName: string) {
  useEffect(() => {
    if (process.env.NODE_ENV === 'production') {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.entryType === 'measure') {
            // 发送到监控平台
            analytics.track('render_performance', {
              component: componentName,
              duration: entry.duration,
              timestamp: entry.startTime
            });
          }
        }
      });
      
      observer.observe({ entryTypes: ['measure'] });
      
      return () => observer.disconnect();
    }
  }, [componentName]);
}
 
// 使用示例
function MyComponent() {
  usePerformanceMonitor('MyComponent');
  
  performance.mark('MyComponent-start');
  
  // 组件逻辑...
  
  useEffect(() => {
    performance.mark('MyComponent-end');
    performance.measure('MyComponent', 'MyComponent-start', 'MyComponent-end');
  });
}

组件渲染优化

React.memo 的正确使用

// ❌ 错误使用:props 总是变化
const ExpensiveComponent = React.memo(({ items }) => {
  return items.map(item => <Item key={item.id} item={item} />);
});
 
function Parent() {
  const [count, setCount] = useState(0);
  const items = [{ id: 1, name: 'Item 1' }];
  
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* items 每次都是新对象,memo 无效 */}
      <ExpensiveComponent items={items} />
    </>
  );
}
 
// ✅ 正确使用:稳定 props
const ExpensiveComponent = React.memo(({ items }) => {
  return items.map(item => <Item key={item.id} item={item} />);
}, (prevProps, nextProps) => {
  // 自定义比较函数
  if (prevProps.items.length !== nextProps.items.length) {
    return false; // props 不同,需要重新渲染
  }
  
  return prevProps.items.every((item, index) => 
    item.id === nextProps.items[index].id
  );
});
 
function Parent() {
  const [count, setCount] = useState(0);
  const items = useMemo(() => [{ id: 1, name: 'Item 1' }], []);
  
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveComponent items={items} />
    </>
  );
}

子组件提取策略

// ❌ 问题:每次渲染都创建新的子组件
function Parent({ data }) {
  const [filter, setFilter] = useState('');
  
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {data.map(item => (
        <ExpensiveItem 
          key={item.id} 
          item={item}
          onClick={() => handleClick(item.id)} // 新函数
        />
      ))}
    </div>
  );
}
 
// ✅ 优化:提取子组件,稳定引用
const ExpensiveItem = React.memo(({ item, onClick }) => {
  return <div onClick={onClick}>{item.name}</div>;
});
 
function Parent({ data }) {
  const [filter, setFilter] = useState('');
  
  // 使用 useCallback 稳定函数引用
  const handleClick = useCallback((id: string) => {
    // 处理点击
  }, []);
  
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {data.map(item => (
        <ExpensiveItem 
          key={item.id} 
          item={item}
          onClick={handleClick}
        />
      ))}
    </div>
  );
}
 
// ✅ 进一步优化:分离列表组件
const ItemList = React.memo(({ items, onItemClick }) => {
  return (
    <>
      {items.map(item => (
        <ExpensiveItem 
          key={item.id} 
          item={item}
          onClick={onItemClick}
        />
      ))}
    </>
  );
});
 
function Parent({ data }) {
  const [filter, setFilter] = useState('');
  const filteredData = useMemo(
    () => data.filter(item => item.name.includes(filter)),
    [data, filter]
  );
  
  const handleClick = useCallback((id: string) => {
    // 处理点击
  }, []);
  
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <ItemList items={filteredData} onItemClick={handleClick} />
    </div>
  );
}

虚拟列表优化

对于长列表,虚拟滚动是必需的:

import { useVirtualizer } from '@tanstack/react-virtual';
 
function VirtualList({ items }) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // 估算每个项目高度
    overscan: 5, // 渲染额外的项目以平滑滚动
  });
  
  return (
    <div
      ref={parentRef}
      style={{ height: '400px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

代码分割与懒加载

路由级别的代码分割

// 使用 React.lazy 进行代码分割
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 Blog = lazy(() => import('./pages/Blog'));
 
function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/blog" element={<Blog />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

组件级别的懒加载

// 条件加载重型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
 
function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>显示图表</button>
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}
 
// 使用 Intersection Observer 实现视图内懒加载
function LazyImage({ src, alt }) {
  const imgRef = useRef<HTMLImageElement>(null);
  const [isInView, setIsInView] = useState(false);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );
    
    if (imgRef.current) {
      observer.observe(imgRef.current);
    }
    
    return () => observer.disconnect();
  }, []);
  
  return (
    <img
      ref={imgRef}
      src={isInView ? src : undefined}
      alt={alt}
      loading="lazy"
    />
  );
}

动态导入优化

// 预加载组件(鼠标悬停时)
function LinkWithPreload({ to, children }) {
  const preloadComponent = () => {
    // 预加载但不同步执行
    import(`./pages/${to}`);
  };
  
  return (
    <Link 
      to={to}
      onMouseEnter={preloadComponent}
    >
      {children}
    </Link>
  );
}
 
// 使用动态导入加载第三方库
async function loadHeavyLibrary() {
  const module = await import('heavy-library');
  return module.default;
}
 
function Component() {
  const [Library, setLibrary] = useState(null);
  
  useEffect(() => {
    loadHeavyLibrary().then(setLibrary);
  }, []);
  
  if (!Library) return <Loading />;
  
  return <Library />;
}

状态管理优化

状态拆分策略

// ❌ 问题:所有状态放在一起
function Component() {
  const [state, setState] = useState({
    user: null,
    posts: [],
    settings: {},
    ui: { modal: false, sidebar: true }
  });
  
  // 更新 UI 状态会导致所有组件重新渲染
  const toggleModal = () => setState(s => ({
    ...s,
    ui: { ...s.ui, modal: !s.ui.modal }
  }));
}
 
// ✅ 优化:按更新频率拆分状态
function Component() {
  // 不常变化的数据
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  // 频繁变化的 UI 状态
  const [modal, setModal] = useState(false);
  const [sidebar, setSidebar] = useState(true);
  
  const toggleModal = () => setModal(v => !v);
}

Context 性能优化

// ❌ 问题:所有状态在一个 Context
const AppContext = createContext();
 
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [locale, setLocale] = useState('zh');
  
  // 任何状态变化都会导致所有消费者重新渲染
  return (
    <AppContext.Provider value={{ user, theme, locale, setUser, setTheme, setLocale }}>
      {children}
    </AppContext.Provider>
  );
}
 
// ✅ 优化:拆分 Context
const UserContext = createContext();
const ThemeContext = createContext();
const LocaleContext = createContext();
 
// 或者使用选择器模式
function createSelectorContext<T>() {
  const Context = createContext<T | null>(null);
  
  const Provider = ({ value, children }) => (
    <Context.Provider value={value}>{children}</Context.Provider>
  );
  
  const useSelector = <R,>(selector: (value: T) => R) => {
    const value = useContext(Context);
    if (value === null) throw new Error('Provider missing');
    
    // 只在选择的值变化时更新
    return useMemo(() => selector(value), [value, selector]);
  };
  
  return { Provider, useSelector };
}

渲染性能优化技巧

避免不必要的重新渲染

// 使用 React.memo 和 useMemo 组合
const Item = React.memo(({ id, name, onClick }) => {
  console.log(`Rendering item ${id}`);
  return <div onClick={onClick}>{name}</div>;
});
 
function List({ items }) {
  const memoizedItems = useMemo(
    () => items.map(item => ({
      id: item.id,
      name: item.name,
    })),
    [items]
  );
  
  const handleClick = useCallback((id: string) => {
    // 处理点击
  }, []);
  
  return (
    <>
      {memoizedItems.map(item => (
        <Item 
          key={item.id}
          {...item}
          onClick={() => handleClick(item.id)}
        />
      ))}
    </>
  );
}

使用 useDeferredValue 延迟更新

import { useDeferredValue, useMemo } from 'react';
 
function SearchResults({ query }) {
  // 延迟更新,保持 UI 响应
  const deferredQuery = useDeferredValue(query);
  
  const results = useMemo(() => {
    return expensiveSearch(deferredQuery);
  }, [deferredQuery]);
  
  // query 变化时立即显示旧结果,后台计算新结果
  return <ResultsList results={results} />;
}

批量更新优化

// React 18 自动批量更新(包括异步操作)
function Component() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  function handleClick() {
    // React 18: 这两个更新会被批量处理,只渲染一次
    fetch('/api').then(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
    });
  }
}
 
// 手动批量更新
import { unstable_batchedUpdates } from 'react-dom';
 
unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  setName('New Name');
});

内存泄漏预防

// 清理订阅和定时器
function Component() {
  useEffect(() => {
    const subscription = subscribe();
    const timer = setInterval(() => {
      // 定时任务
    }, 1000);
    
    return () => {
      subscription.unsubscribe();
      clearInterval(timer);
    };
  }, []);
  
  // 清理事件监听器
  useEffect(() => {
    const handleResize = () => {
      // 处理窗口大小变化
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
}
 
// 避免在已卸载的组件上设置状态
function useSafeState<T>(initialValue: T) {
  const [state, setState] = useState(initialValue);
  const isMountedRef = useRef(true);
  
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  
  const safeSetState = useCallback((value: T | ((prev: T) => T)) => {
    if (isMountedRef.current) {
      setState(value);
    }
  }, []);
  
  return [state, safeSetState] as const;
}

性能优化检查清单

  1. 测量性能:使用 React DevTools Profiler 识别瓶颈
  2. 组件优化:合理使用 React.memo、useMemo、useCallback
  3. 代码分割:路由和组件级别的懒加载
  4. 虚拟列表:长列表使用虚拟滚动
  5. 状态管理:拆分状态,优化 Context
  6. 图片优化:懒加载、WebP 格式、响应式图片
  7. Bundle 分析:使用 webpack-bundle-analyzer 分析打包大小
  8. 生产构建:启用代码压缩、Tree Shaking

记住:过早优化是万恶之源。先测量,再优化,持续监控。