📚 现代前端开发 目录
阶段3:React
第 20 / 35 课

性能优化

让React应用飞起来

📖 核心概念

💻 代码实现

性能优化技巧 ✅ tsx
import { memo, useMemo, useCallback, lazy, Suspense, useState, useRef, useEffect } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';

// ===== 1. React.memo - 避免不必要的重渲染 =====
interface ExpensiveItemProps {
  id: string;
  name: string;
  onSelect: (id: string) => void;
}

const ExpensiveItem = memo(function ExpensiveItem({ id, name, onSelect }: ExpensiveItemProps) {
  console.log(\`渲染: \${name}\`);
  return (
    <div onClick={() => onSelect(id)} className="item">
      {name}
    </div>
  );
}, (prev, next) => prev.id === next.id && prev.name === next.name);

// ===== 2. 虚拟列表 - 渲染10000+项无压力 =====
function VirtualList({ items }: { items: { id: string; name: string }[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
    overscan: 5, // 预渲染5个
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: \`\${virtualizer.getTotalSize()}px\`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(vItem => (
          <div
            key={vItem.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: \`translateY(\${vItem.start}px)\`,
              height: \`\${vItem.size}px\`,
            }}
          >
            {items[vItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

// ===== 3. 代码分割 =====
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>控制台</h1>
      <Suspense fallback={<Skeleton />}>
        <HeavyComponent />
      </Suspense>
      <button onClick={() => setShowChart(true)}>显示图表</button>
      {showChart && (
        <Suspense fallback={<div>图表加载中...</div>}>
          <Chart />
        </Suspense>
      )}
    </div>
  );
}

// ===== 4. 图片优化 =====
function OptimizedImage({ src, alt, width, height }: ImageProps) {
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && imgRef.current) {
        imgRef.current.src = imgRef.current.dataset.src!;
        observer.unobserve(entry.target);
      }
    });
    if (imgRef.current) observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div style={{ width, height, background: '#1e293b' }}>
      {!loaded && <Skeleton />}
      <img
        ref={imgRef}
        data-src={src}
        alt={alt}
        onLoad={() => setLoaded(true)}
        style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
        loading="lazy"
        decoding="async"
      />
    </div>
  );
}
性能分析与调试 ✅ tsx
/** 
 * React DevTools Profiler 使用指南:
 * 1. 安装React DevTools浏览器扩展
 * 2. 打开Profiler标签页
 * 3. 点击录制按钮
 * 4. 操作你的应用
 * 5. 停止录制,分析渲染数据
 * 
 * 关键指标:
 * - Render time: 组件渲染耗时
 * - Why did this render?: 渲染原因
 * - Flame chart: 渲染树可视化
 */

// 自定义性能监控Hook
function useRenderCount(componentName: string) {
  const count = useRef(0);
  count.current++;
  useEffect(() => {
    if (count.current > 1) {
      console.log(\`[\${componentName}] 渲染次数: \${count.current}\`);
    }
  });
}

// 状态下推 - 避免整棵树重渲染
// ❌ 不好:所有子组件都会重渲染
function BadList() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  return items.map(item => (
    <Item
      key={item.id}
      item={item}
      selected={selectedId === item.id}
      onSelect={setSelectedId}
    />
  ));
}

// ✅ 好:每个Item管理自己的选中状态
function GoodItem({ item }: { item: Item }) {
  const [selected, setSelected] = useState(false);
  return <div onClick={() => setSelected(!selected)}>...</div>;
}

// 使用React.memo + 稳定引用
function Parent() {
  const [count, setCount] = useState(0);

  // useCallback保持函数引用稳定
  const handleSelect = useCallback((id: string) => {
    // 处理选择逻辑
  }, []); // 无依赖,引用永不变

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <MemoizedChild onSelect={handleSelect} />
    </div>
  );
}

// 批量状态更新 (React 18自动批处理)
function BatchUpdates() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  // React 18: 只触发一次渲染
  const handleClick = () => {
    setA(a + 1);
    setB(b + 1);
  };

  // 在异步代码中也自动批处理 (React 18新特性)
  const handleAsync = async () => {
    const data = await fetchData();
    setA(data.a);  // 不会立即渲染
    setB(data.b);  // 不会立即渲染
    // 两次setState合并为一次渲染
  };
}

🎯 练习任务

🏆 成就解锁
性能优化师 — 掌握React性能优化技巧,打造丝滑的用户体验