Skip to content

内存泄漏识别与修复 - React应用内存管理指南

1. 什么是内存泄漏

1.1 内存泄漏定义

typescript
const memoryLeakDefinition = {
  定义: '应用程序不再需要的内存未被释放,导致可用内存逐渐减少',
  表现: [
    '页面响应变慢',
    '浏览器标签页崩溃',
    '内存使用持续增长',
    '垃圾回收频繁触发'
  ],
  常见原因: [
    '未清理的事件监听器',
    '未清理的定时器',
    '未关闭的连接',
    'DOM引用未释放',
    '闭包持有大对象',
    '全局变量累积'
  ]
};

1.2 JavaScript内存管理基础

javascript
// 垃圾回收示例
function createUser() {
  let user = {  // 分配内存
    name: 'John',
    age: 30
  };
  
  return user;
}

let userData = createUser();  // user对象被引用
userData = null;  // 取消引用,对象可被垃圾回收

// 闭包保持引用
function createClosure() {
  let largeArray = new Array(1000000).fill('data');  // 分配大量内存
  
  return function() {
    // 闭包持有largeArray的引用
    console.log(largeArray.length);
  };
}

const closure = createClosure();  // largeArray不会被回收
// closure = null;  // 释放闭包后,largeArray才能被回收

1.3 React中的内存泄漏

jsx
// ❌ 典型内存泄漏: 组件卸载后仍然setState
function BadComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        // 如果组件已卸载,这里会内存泄漏并警告
        setData(data);
      });
  }, []);
  
  return <div>{data}</div>;
}

// ✅ 正确: 检查组件是否已卸载
function GoodComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let cancelled = false;
    
    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
        }
      });
    
    return () => {
      cancelled = true;
    };
  }, []);
  
  return <div>{data}</div>;
}

2. 常见内存泄漏场景

2.1 未清理的事件监听器

jsx
// ❌ 内存泄漏: 忘记移除事件监听器
function BadEventListener() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleClick = () => {
      setCount(c => c + 1);
    };
    
    document.addEventListener('click', handleClick);
    
    // 忘记return清理函数!
    // 组件卸载后,监听器仍然存在,持有组件引用
  }, []);
  
  return <div>Clicks: {count}</div>;
}

// ✅ 正确: 清理事件监听器
function GoodEventListener() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleClick = () => {
      setCount(c => c + 1);
    };
    
    document.addEventListener('click', handleClick);
    
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []);
  
  return <div>Clicks: {count}</div>;
}

// ✅ 自定义Hook封装
function useClickCount() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleClick = () => setCount(c => c + 1);
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, []);
  
  return count;
}

function ComponentUsingHook() {
  const clickCount = useClickCount();
  return <div>Clicks: {clickCount}</div>;
}

2.2 未清理的定时器

jsx
// ❌ 内存泄漏: 定时器未清理
function BadTimer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    // 忘记清理!
    // 组件卸载后,定时器仍在运行
  }, []);
  
  return <div>{seconds}s</div>;
}

// ✅ 正确: 清理定时器
function GoodTimer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    return () => {
      clearInterval(interval);
    };
  }, []);
  
  return <div>{seconds}s</div>;
}

// ❌ 多个定时器累积
function BadMultipleTimers({ interval }) {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 每次interval变化都创建新定时器
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, interval);
    
    // 忘记清理旧定时器!
  }, [interval]);
  
  return <div>{count}</div>;
}

// ✅ 正确: 清理旧定时器
function GoodMultipleTimers({ interval }) {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, interval);
    
    return () => {
      clearInterval(timer);
    };
  }, [interval]);
  
  return <div>{count}</div>;
}

2.3 未关闭的连接

jsx
// ❌ WebSocket未关闭
function BadWebSocket({ url }) {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };
    
    // 忘记关闭WebSocket!
  }, [url]);
  
  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  );
}

// ✅ 正确: 关闭WebSocket
function GoodWebSocket({ url }) {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };
    
    return () => {
      ws.close();
    };
  }, [url]);
  
  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  );
}

// ❌ EventSource未关闭
function BadSSE() {
  const [updates, setUpdates] = useState([]);
  
  useEffect(() => {
    const eventSource = new EventSource('/api/updates');
    
    eventSource.onmessage = (event) => {
      setUpdates(prev => [...prev, event.data]);
    };
    
    // 忘记关闭!
  }, []);
  
  return <div>{updates.length} updates</div>;
}

// ✅ 正确: 关闭EventSource
function GoodSSE() {
  const [updates, setUpdates] = useState([]);
  
  useEffect(() => {
    const eventSource = new EventSource('/api/updates');
    
    eventSource.onmessage = (event) => {
      setUpdates(prev => [...prev, event.data]);
    };
    
    return () => {
      eventSource.close();
    };
  }, []);
  
  return <div>{updates.length} updates</div>;
}

2.4 DOM引用泄漏

jsx
// ❌ 持有DOM引用
function BadDOMRef() {
  const elementsRef = useRef([]);
  
  useEffect(() => {
    // 收集所有按钮元素
    const buttons = document.querySelectorAll('button');
    elementsRef.current = Array.from(buttons);
    
    // 组件卸载后,仍持有DOM引用
  }, []);
  
  const logElements = () => {
    console.log(elementsRef.current);
  };
  
  return <button onClick={logElements}>Log Elements</button>;
}

// ✅ 正确: 清理DOM引用
function GoodDOMRef() {
  const elementsRef = useRef([]);
  
  useEffect(() => {
    const buttons = document.querySelectorAll('button');
    elementsRef.current = Array.from(buttons);
    
    return () => {
      elementsRef.current = [];  // 清理引用
    };
  }, []);
  
  const logElements = () => {
    console.log(elementsRef.current);
  };
  
  return <button onClick={logElements}>Log Elements</button>;
}

// ❌ IntersectionObserver未断开
function BadIntersectionObserver() {
  const ref = useRef(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      console.log(entries);
    });
    
    if (ref.current) {
      observer.observe(ref.current);
    }
    
    // 忘记断开观察!
  }, []);
  
  return <div ref={ref}>Observed Element</div>;
}

// ✅ 正确: 断开观察
function GoodIntersectionObserver() {
  const ref = useRef(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      console.log(entries);
    });
    
    const element = ref.current;
    if (element) {
      observer.observe(element);
    }
    
    return () => {
      if (element) {
        observer.unobserve(element);
      }
      observer.disconnect();
    };
  }, []);
  
  return <div ref={ref}>Observed Element</div>;
}

2.5 闭包持有大对象

jsx
// ❌ 闭包持有大对象
function BadClosure() {
  const [count, setCount] = useState(0);
  
  // 大对象
  const largeData = useMemo(() => {
    return new Array(1000000).fill({ value: 'data' });
  }, []);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 闭包捕获了largeData
      console.log('Count:', count, 'Data size:', largeData.length);
      setCount(count + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [count, largeData]);  // largeData在依赖中
  
  return <div>{count}</div>;
}

// ✅ 解决方案1: 避免在闭包中使用大对象
function ClosureSolution1() {
  const [count, setCount] = useState(0);
  
  const largeData = useMemo(() => {
    return new Array(1000000).fill({ value: 'data' });
  }, []);
  
  // 保存大小而不是整个对象
  const dataSize = largeData.length;
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Count:', count, 'Data size:', dataSize);
      setCount(count + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [count, dataSize]);  // 只依赖数字
  
  return <div>{count}</div>;
}

// ✅ 解决方案2: 使用useRef
function ClosureSolution2() {
  const [count, setCount] = useState(0);
  
  const largeDataRef = useRef(
    new Array(1000000).fill({ value: 'data' })
  );
  
  useEffect(() => {
    const timer = setInterval(() => {
      // ref不需要在依赖中
      console.log('Count:', count, 'Data size:', largeDataRef.current.length);
      setCount(c => c + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  // 组件卸载时清理
  useEffect(() => {
    return () => {
      largeDataRef.current = null;
    };
  }, []);
  
  return <div>{count}</div>;
}

2.6 第三方库集成

jsx
// ❌ Chart.js未销毁
function BadChart({ data }) {
  const chartRef = useRef(null);
  const chartInstanceRef = useRef(null);
  
  useEffect(() => {
    if (chartRef.current) {
      chartInstanceRef.current = new Chart(chartRef.current, {
        type: 'line',
        data: data
      });
    }
    
    // 忘记销毁图表实例!
  }, [data]);
  
  return <canvas ref={chartRef} />;
}

// ✅ 正确: 销毁图表实例
function GoodChart({ data }) {
  const chartRef = useRef(null);
  const chartInstanceRef = useRef(null);
  
  useEffect(() => {
    if (chartRef.current) {
      // 先销毁旧实例
      if (chartInstanceRef.current) {
        chartInstanceRef.current.destroy();
      }
      
      chartInstanceRef.current = new Chart(chartRef.current, {
        type: 'line',
        data: data
      });
    }
    
    return () => {
      if (chartInstanceRef.current) {
        chartInstanceRef.current.destroy();
        chartInstanceRef.current = null;
      }
    };
  }, [data]);
  
  return <canvas ref={chartRef} />;
}

// ❌ React Query未清理缓存
function BadReactQuery() {
  const { data } = useQuery({
    queryKey: ['huge-dataset'],
    queryFn: fetchHugeDataset,
    cacheTime: Infinity  // 永久缓存!
  });
  
  return <div>{data?.length} items</div>;
}

// ✅ 正确: 设置合理的缓存时间
function GoodReactQuery() {
  const { data } = useQuery({
    queryKey: ['huge-dataset'],
    queryFn: fetchHugeDataset,
    cacheTime: 5 * 60 * 1000,  // 5分钟后清理
    staleTime: 1 * 60 * 1000   // 1分钟后标记为过期
  });
  
  return <div>{data?.length} items</div>;
}

3. 内存泄漏检测

3.1 Chrome DevTools Memory Profiler

typescript
const memoryProfilerGuide = {
  步骤: [
    '1. 打开Chrome DevTools',
    '2. 切换到Memory选项卡',
    '3. 选择"Heap snapshot"',
    '4. 点击"Take snapshot"',
    '5. 执行操作(如打开/关闭组件)',
    '6. 再次Take snapshot',
    '7. 选择"Comparison"视图比较两个快照',
    '8. 查看增长的对象和引用链'
  ],
  关键指标: [
    'Shallow Size: 对象本身的大小',
    'Retained Size: 对象及其引用的总大小',
    'Distance: 到GC根的距离',
    'Detached DOM: 已分离但未释放的DOM节点'
  ],
  常见泄漏模式: [
    'Detached DOM nodes',
    '过多的闭包',
    '大的数组/对象持续增长',
    '未清理的事件监听器'
  ]
};

3.2 Performance Monitor

typescript
const performanceMonitorGuide = {
  使用: [
    '1. Chrome DevTools -> More tools -> Performance monitor',
    '2. 观察"JS heap size"曲线',
    '3. 正常应用: 锯齿状(GC后下降)',
    '4. 内存泄漏: 持续上升'
  ],
  指标: [
    'JS heap size: JavaScript堆内存大小',
    'DOM Nodes: DOM节点数量',
    'JS event listeners: 事件监听器数量',
    'CPU usage: CPU使用率'
  ]
};

3.3 自定义内存监控

jsx
// 创建内存监控Hook
function useMemoryMonitor(componentName) {
  useEffect(() => {
    if (performance.memory) {
      const logMemory = () => {
        const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory;
        console.log(`[Memory] ${componentName}:`, {
          used: `${(usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
          total: `${(totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
          limit: `${(jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`,
          usage: `${((usedJSHeapSize / jsHeapSizeLimit) * 100).toFixed(2)}%`
        });
      };
      
      logMemory();
      const interval = setInterval(logMemory, 5000);
      
      return () => {
        clearInterval(interval);
        console.log(`[Memory] ${componentName} unmounted`);
      };
    }
  }, [componentName]);
}

// 使用
function MonitoredComponent() {
  useMemoryMonitor('MonitoredComponent');
  const [items, setItems] = useState([]);
  
  const addItems = () => {
    setItems(prev => [...prev, ...new Array(10000).fill('item')]);
  };
  
  return (
    <div>
      <button onClick={addItems}>Add 10k Items</button>
      <p>{items.length} items</p>
    </div>
  );
}

3.4 监听器泄漏检测

jsx
// 检测事件监听器泄漏
function useEventListenerDetector() {
  useEffect(() => {
    const originalAddEventListener = EventTarget.prototype.addEventListener;
    const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
    
    const listeners = new Map();
    let listenerId = 0;
    
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      const id = listenerId++;
      listeners.set(id, {
        target: this,
        type,
        listener,
        options,
        stack: new Error().stack
      });
      
      console.log(`[Listener Added] #${id}:`, type, 'Total:', listeners.size);
      
      return originalAddEventListener.call(this, type, listener, options);
    };
    
    EventTarget.prototype.removeEventListener = function(type, listener, options) {
      // 查找并移除
      for (const [id, item] of listeners.entries()) {
        if (item.target === this && item.type === type && item.listener === listener) {
          listeners.delete(id);
          console.log(`[Listener Removed] #${id}:`, type, 'Total:', listeners.size);
          break;
        }
      }
      
      return originalRemoveEventListener.call(this, type, listener, options);
    };
    
    // 定期报告泄漏
    const reportInterval = setInterval(() => {
      if (listeners.size > 0) {
        console.warn(`[Listener Leak] ${listeners.size} listeners not removed:`, 
          Array.from(listeners.values()).map(l => ({
            type: l.type,
            target: l.target.constructor.name
          }))
        );
      }
    }, 10000);
    
    return () => {
      clearInterval(reportInterval);
      EventTarget.prototype.addEventListener = originalAddEventListener;
      EventTarget.prototype.removeEventListener = originalRemoveEventListener;
    };
  }, []);
}

// 在开发环境使用
function App() {
  if (process.env.NODE_ENV === 'development') {
    useEventListenerDetector();
  }
  
  return <YourApp />;
}

4. 修复策略

4.1 cleanup函数模式

jsx
// 标准cleanup模式
function useCleanupPattern(subscribe) {
  useEffect(() => {
    // 1. 设置资源
    const resource = subscribe();
    
    // 2. 返回清理函数
    return () => {
      // 3. 清理资源
      resource.unsubscribe();
    };
  }, [subscribe]);
}

// 多个cleanup
function useMultipleCleanup() {
  useEffect(() => {
    // 设置多个资源
    const timer = setInterval(() => {}, 1000);
    const listener = () => {};
    window.addEventListener('resize', listener);
    
    // 清理所有资源
    return () => {
      clearInterval(timer);
      window.removeEventListener('resize', listener);
    };
  }, []);
}

// 条件cleanup
function useConditionalCleanup(shouldSubscribe) {
  useEffect(() => {
    if (!shouldSubscribe) return;
    
    const subscription = subscribe();
    
    return () => {
      subscription.unsubscribe();
    };
  }, [shouldSubscribe]);
}

4.2 AbortController模式

jsx
// 使用AbortController取消请求
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    setLoading(true);
    setError(null);
    
    fetch(url, {
      signal: abortController.signal
    })
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err);
          setLoading(false);
        }
      });
    
    return () => {
      abortController.abort();
    };
  }, [url]);
  
  return { data, loading, error };
}

// 多个请求共享AbortController
function useMultipleFetch(urls) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    Promise.all(
      urls.map(url => 
        fetch(url, { signal: abortController.signal })
          .then(res => res.json())
      )
    )
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });
    
    return () => {
      abortController.abort();
    };
  }, [urls]);
  
  return results;
}

4.3 WeakMap/WeakSet模式

jsx
// 使用WeakMap避免内存泄漏
function useWeakMapCache() {
  const cache = useRef(new WeakMap());
  
  const cacheValue = useCallback((key, value) => {
    // WeakMap的key必须是对象
    // 当key对象被回收时,对应的value也会被回收
    cache.current.set(key, value);
  }, []);
  
  const getValue = useCallback((key) => {
    return cache.current.get(key);
  }, []);
  
  return { cacheValue, getValue };
}

// 使用WeakSet跟踪对象
function useWeakSetTracker() {
  const tracked = useRef(new WeakSet());
  
  const track = useCallback((obj) => {
    tracked.current.add(obj);
  }, []);
  
  const isTracked = useCallback((obj) => {
    return tracked.current.has(obj);
  }, []);
  
  return { track, isTracked };
}

4.4 资源池模式

jsx
// 创建对象池避免频繁创建
class ObjectPool {
  constructor(createFn, resetFn, size = 10) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
    this.active = new Set();
    
    // 预创建对象
    for (let i = 0; i < size; i++) {
      this.pool.push(this.createFn());
    }
  }
  
  acquire() {
    let obj = this.pool.pop();
    if (!obj) {
      obj = this.createFn();
    }
    this.active.add(obj);
    return obj;
  }
  
  release(obj) {
    if (this.active.has(obj)) {
      this.active.delete(obj);
      this.resetFn(obj);
      this.pool.push(obj);
    }
  }
  
  clear() {
    this.pool = [];
    this.active.clear();
  }
}

// 使用对象池
function useObjectPool() {
  const poolRef = useRef(
    new ObjectPool(
      () => ({ data: null, timestamp: null }),
      (obj) => { obj.data = null; obj.timestamp = null; },
      20
    )
  );
  
  useEffect(() => {
    return () => {
      poolRef.current.clear();
    };
  }, []);
  
  const acquire = useCallback(() => {
    return poolRef.current.acquire();
  }, []);
  
  const release = useCallback((obj) => {
    poolRef.current.release(obj);
  }, []);
  
  return { acquire, release };
}

5. 最佳实践

5.1 组件生命周期管理

jsx
// 统一的资源管理Hook
function useResourceManager() {
  const resourcesRef = useRef([]);
  
  const registerResource = useCallback((cleanup) => {
    resourcesRef.current.push(cleanup);
    
    // 返回取消注册函数
    return () => {
      const index = resourcesRef.current.indexOf(cleanup);
      if (index > -1) {
        resourcesRef.current.splice(index, 1);
      }
    };
  }, []);
  
  useEffect(() => {
    return () => {
      // 组件卸载时清理所有资源
      resourcesRef.current.forEach(cleanup => cleanup());
      resourcesRef.current = [];
    };
  }, []);
  
  return registerResource;
}

// 使用
function Component() {
  const registerResource = useResourceManager();
  
  useEffect(() => {
    const timer = setInterval(() => {}, 1000);
    
    // 注册清理函数
    registerResource(() => clearInterval(timer));
  }, [registerResource]);
  
  useEffect(() => {
    const listener = () => {};
    window.addEventListener('resize', listener);
    
    registerResource(() => {
      window.removeEventListener('resize', listener);
    });
  }, [registerResource]);
  
  return <div>Component</div>;
}

5.2 防御式编程

jsx
// 安全的setState
function useSafeState(initialState) {
  const [state, setState] = useState(initialState);
  const mountedRef = useRef(false);
  
  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
    };
  }, []);
  
  const safeSetState = useCallback((newState) => {
    if (mountedRef.current) {
      setState(newState);
    }
  }, []);
  
  return [state, safeSetState];
}

// 安全的异步操作
function useSafeAsync(asyncFn, dependencies) {
  const [data, setData] = useSafeState(null);
  const [loading, setLoading] = useSafeState(true);
  const [error, setError] = useSafeState(null);
  
  useEffect(() => {
    let cancelled = false;
    
    setLoading(true);
    setError(null);
    
    asyncFn()
      .then(result => {
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
    
    return () => {
      cancelled = true;
    };
  }, dependencies);
  
  return { data, loading, error };
}

5.3 性能监控

jsx
// 创建性能监控系统
function usePerformanceMonitor(componentName) {
  const metricsRef = useRef({
    renderCount: 0,
    lastRenderTime: Date.now(),
    mountTime: Date.now(),
    memorySnapshots: []
  });
  
  // 记录渲染
  metricsRef.current.renderCount++;
  metricsRef.current.lastRenderTime = Date.now();
  
  // 记录内存
  useEffect(() => {
    if (performance.memory) {
      metricsRef.current.memorySnapshots.push({
        time: Date.now(),
        used: performance.memory.usedJSHeapSize
      });
      
      // 只保留最近50个快照
      if (metricsRef.current.memorySnapshots.length > 50) {
        metricsRef.current.memorySnapshots.shift();
      }
    }
  });
  
  // 卸载时报告
  useEffect(() => {
    return () => {
      const lifetime = Date.now() - metricsRef.current.mountTime;
      const snapshots = metricsRef.current.memorySnapshots;
      
      if (snapshots.length > 1) {
        const first = snapshots[0].used;
        const last = snapshots[snapshots.length - 1].used;
        const growth = last - first;
        const growthRate = (growth / lifetime) * 1000;  // bytes/second
        
        console.log(`[Performance] ${componentName}:`, {
          lifetime: `${(lifetime / 1000).toFixed(2)}s`,
          renders: metricsRef.current.renderCount,
          memoryGrowth: `${(growth / 1024 / 1024).toFixed(2)} MB`,
          growthRate: `${(growthRate / 1024).toFixed(2)} KB/s`
        });
        
        // 警告内存泄漏
        if (growthRate > 100 * 1024) {  // > 100KB/s
          console.warn(`[Memory Leak Warning] ${componentName} might have a memory leak`);
        }
      }
    };
  }, [componentName]);
  
  return metricsRef.current;
}

6. 测试内存泄漏

6.1 自动化测试

jsx
// Jest测试内存泄漏
import { render, unmountComponentAtNode } from '@testing-library/react';
import { act } from 'react-dom/test-utils';

describe('Memory Leak Tests', () => {
  let container = null;
  
  beforeEach(() => {
    container = document.createElement('div');
    document.body.appendChild(container);
  });
  
  afterEach(() => {
    unmountComponentAtNode(container);
    container.remove();
    container = null;
  });
  
  test('component cleans up event listeners', () => {
    const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
    const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
    
    act(() => {
      render(<ComponentWithListener />, container);
    });
    
    expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
    
    act(() => {
      unmountComponentAtNode(container);
    });
    
    expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
    
    addEventListenerSpy.mockRestore();
    removeEventListenerSpy.mockRestore();
  });
  
  test('component cleans up timers', () => {
    jest.useFakeTimers();
    
    act(() => {
      render(<ComponentWithTimer />, container);
    });
    
    const timerCount = jest.getTimerCount();
    expect(timerCount).toBeGreaterThan(0);
    
    act(() => {
      unmountComponentAtNode(container);
    });
    
    expect(jest.getTimerCount()).toBe(0);
    
    jest.useRealTimers();
  });
});

6.2 Cypress E2E测试

javascript
// cypress/integration/memory-leak.spec.js
describe('Memory Leak Detection', () => {
  it('should not leak memory on repeated mount/unmount', () => {
    cy.visit('/test-page');
    
    // 记录初始内存
    cy.window().then((win) => {
      const initialMemory = win.performance.memory.usedJSHeapSize;
      
      // 重复mount/unmount 10次
      for (let i = 0; i < 10; i++) {
        cy.get('[data-testid="mount-component"]').click();
        cy.wait(100);
        cy.get('[data-testid="unmount-component"]').click();
        cy.wait(100);
      }
      
      // 强制GC (需要Chrome启动参数: --js-flags="--expose-gc")
      if (win.gc) {
        win.gc();
      }
      
      cy.wait(1000);
      
      // 检查内存增长
      cy.window().then((win) => {
        const finalMemory = win.performance.memory.usedJSHeapSize;
        const growth = finalMemory - initialMemory;
        const growthMB = growth / 1024 / 1024;
        
        cy.log(`Memory growth: ${growthMB.toFixed(2)} MB`);
        
        // 内存增长应小于10MB
        expect(growthMB).to.be.lessThan(10);
      });
    });
  });
});

7. 总结

内存泄漏是React应用中需要重点关注的问题:

  1. 常见原因: 未清理的监听器/定时器/连接、DOM引用、闭包
  2. 检测方法: Chrome DevTools、Performance Monitor、自定义监控
  3. 修复策略: cleanup函数、AbortController、WeakMap、对象池
  4. 最佳实践: 资源管理、防御式编程、性能监控
  5. 测试: 自动化测试、E2E测试

掌握这些知识可以有效避免和修复内存泄漏,构建高性能的React应用。