一、内存泄漏是个什么鬼?

咱们做React开发的,最怕的就是应用用着用着越来越卡,最后直接崩溃。这种情况十有八九是内存泄漏在搞鬼。简单来说,内存泄漏就是该释放的内存没释放,就像你家水龙头没关紧,水一直在流,最后水费爆表。

在React里,常见的内存泄漏场景包括:事件监听没卸载、定时器没清除、全局变量滥用、大对象没释放等。这些都会导致内存占用越来越高,轻则卡顿,重则崩溃。

二、如何发现内存泄漏?

1. Chrome开发者工具是神器

打开Chrome DevTools,到Memory面板,用Heap Snapshot功能拍个快照。操作步骤:

  1. 打开你的React应用
  2. F12打开开发者工具
  3. 切换到Memory标签页
  4. 点击"Take heap snapshot"
  5. 操作你的应用
  6. 再拍一个快照
  7. 对比两个快照

2. 示例代码(React + TypeScript技术栈)

import React, { useEffect, useState } from 'react';

const LeakyComponent = () => {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    // 错误示范:定时器没清理
    const timer = setInterval(() => {
      fetchData();
    }, 1000);

    // 正确做法应该这样:
    // return () => clearInterval(timer);
    
    const fetchData = async () => {
      // 模拟大数据请求
      const bigData = new Array(1000000).fill({ 
        id: Math.random(),
        content: 'This is some leaky data' 
      });
      setData(bigData);
    };

    fetchData();
  }, []);

  return <div>内存泄漏示例组件</div>;
};

export default LeakyComponent;

这个组件有两个问题:

  1. 定时器没清理,会一直执行
  2. 每次请求都会创建超大数组,旧数据却没释放

三、常见内存泄漏场景及修复

1. 事件监听没卸载

import React, { useEffect } from 'react';

const EventListenerLeak = () => {
  useEffect(() => {
    const handleScroll = () => {
      console.log('Scrolling...');
    };

    window.addEventListener('scroll', handleScroll);
    
    // 忘记卸载事件监听
    // 正确做法:
    // return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return <div style={{ height: '200vh' }}>滚动我看看控制台</div>;
};

2. 订阅没取消

import React, { useEffect, useState } from 'react';
import { fromEvent } from 'rxjs';

const RxJsLeak = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 创建可观察对象
    const subscription = fromEvent(document, 'click')
      .subscribe(() => {
        setCount(c => c + 1);
      });
    
    // 忘记取消订阅
    // 正确做法:
    // return () => subscription.unsubscribe();
  }, []);

  return <div>点击次数: {count}</div>;
};

3. 大对象缓存问题

import React, { useEffect, useState } from 'react';

const BigDataCache = () => {
  const [showData, setShowData] = useState(false);
  // 这个大对象会一直存在内存中
  const bigDataCache = new Array(1000000).fill({
    id: Math.random(),
    content: 'This is some cached data'
  });

  useEffect(() => {
    // 模拟数据使用
    if (showData) {
      console.log(bigDataCache.length);
    }
  }, [showData]);

  return (
    <div>
      <button onClick={() => setShowData(!showData)}>
        {showData ? '隐藏数据' : '显示数据'}
      </button>
    </div>
  );
};

四、高级检测与修复技巧

1. 使用React DevTools检测

React DevTools可以检测组件是否被正确卸载。如果组件已经不在DOM中,但还在内存中,那就是泄漏了。

2. 内存泄漏自动检测工具

可以集成why-did-you-render或memlab这样的工具到开发环境:

// 在项目入口文件添加
import { setConfig } from 'react-query';

setConfig({
  // 开启内存泄漏检测
  queries: {
    staleTime: 30 * 1000,
    cacheTime: 5 * 60 * 1000,
    // 其他配置...
  }
});

3. 使用WeakMap和WeakSet

import React, { useEffect } from 'react';

const WeakMapExample = () => {
  useEffect(() => {
    // 使用WeakMap而不是普通Map
    const weakMap = new WeakMap();
    const bigObject = { /* 大对象 */ };
    
    weakMap.set(bigObject, 'some metadata');
    
    // 当bigObject不再被引用时,会自动被垃圾回收
    // 而普通Map会一直保持对键的强引用
  }, []);

  return <div>WeakMap示例</div>;
};

五、最佳实践总结

  1. 每个useEffect都要考虑清理函数
  2. 避免在全局作用域存储大对象
  3. 使用适当的缓存策略
  4. 定期进行内存检测
  5. 考虑使用不可变数据来避免意外引用

记住,内存泄漏就像房间里的垃圾,不及时清理就会越堆越多。养成良好的编码习惯,才能写出高性能的React应用。