前端内存泄漏排查与预防

🔍 前端内存泄漏排查与预防(极简版)

🎯 一、一句话理解内存泄漏

内存泄漏 = 程序要用的内存越来越多,但用完后不还给系统,最后内存耗尽导致卡顿或崩溃

就像:租了房子不退租还继续租新的,房东(系统)没房子可租了。


📦 二、5个最常见的内存泄漏(必须记住)

1. 全局变量没清理

// ❌ 错误写法:忘记写 let/const
leakedData = fetchBigData(); // 永远在内存中

// ✅ 正确写法
let temporaryData = fetchBigData();
// 用完后
temporaryData = null;

2. 定时器没清理

// ❌ 组件卸载了,定时器还在跑
useEffect(() => {
  setInterval(() => {
    updateData(); // 组件都没了还更新啥?
  }, 1000);
}, []);

// ✅ 正确写法
useEffect(() => {
  const timer = setInterval(() => {
    updateData();
  }, 1000);
  
  return () => clearInterval(timer); // 清理!
}, []);

3. 事件监听没移除

// ❌ 监听后不清理
window.addEventListener('resize', handleResize);

// ✅ 正确写法
useEffect(() => {
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize); // 清理!
  };
}, []);

4. DOM引用没释放

// ❌ 从DOM移除了,但JS还引用着
const elements = [];
elements.push(document.createElement('div')); 
// 即使从页面删除了,elements还引用着div

// ✅ 正确写法
const elements = [];
// 用完后
elements.length = 0; // 清空数组

5. 闭包滥用

// ❌ 函数用完了,但内部引用了外部大变量
function createLeak() {
  const bigData = new Array(1000000).fill('数据');
  
  return function() {
    // 这个函数永远引用着bigData
    console.log('我还在');
  };
}

// ✅ 尽量少用闭包,或及时解除引用

🛠️ 三、React/Vue项目中的实战预防

React项目(记这4条)

// 1. 每个useEffect都要有清理函数
useEffect(() => {
  // 做事情...
  
  return () => {
    // 清理工作:取消请求、移除监听、清理定时器
  };
}, []);

// 2. 用useRef存定时器ID
const timerRef = useRef(null);

useEffect(() => {
  timerRef.current = setInterval(() => {}, 1000);
  
  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  };
}, []);

// 3. 组件卸载时取消请求
useEffect(() => {
  const controller = new AbortController();
  
  fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .then(data => {
      // 更新状态
    });
  
  return () => {
    controller.abort(); // 取消请求
  };
}, []);

// 4. 用React.memo防止不必要渲染
const HeavyComponent = React.memo(({ data }) => {
  return <div>复杂组件</div>;
});

Vue项目(记这3条)

<script>
export default {
  data() {
    return {
      intervalId: null,
      chartInstance: null
    };
  },
  
  mounted() {
    // 1. 定时器要存ID
    this.intervalId = setInterval(() => {}, 1000);
    
    // 2. 第三方库实例要保存
    this.chartInstance = new Chart();
  },
  
  // 3. 必须写beforeDestroy清理
  beforeDestroy() {
    // 清理定时器
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    
    // 清理第三方实例
    if (this.chartInstance) {
      this.chartInstance.destroy();
    }
    
    // 移除事件监听
    window.removeEventListener('resize', this.handleResize);
  }
};
</script>

🔍 四、如何排查内存泄漏(4步法)

第1步:用Chrome DevTools快速定位

1. 打开 Chrome DevTools (F12)
2. 进入 Memory 面板
3. 点击"垃圾桶"图标 - 手动触发垃圾回收
4. 点击"照相机"图标 - 拍内存快照1
5. 操作页面(比如切换路由)
6. 再点击"垃圾桶"图标
7. 再拍内存快照2
8. 对比两次快照,看什么对象增加了

第2步:重点关注这些对象

  • Detached DOM tree - 脱离DOM树的节点
  • EventListener - 事件监听器数量
  • Array, Object - 数组和对象数量激增
  • Closure - 闭包数量

第3步:简单代码检测

// 放在项目里,监控内存
setInterval(() => {
  if (performance.memory) {
    const used = performance.memory.usedJSHeapSize;
    const limit = performance.memory.jsHeapSizeLimit;
    const usage = (used / limit * 100).toFixed(2);
    
    if (usage > 70) {
      console.warn(`⚠️ 内存使用率: ${usage}%`);
    }
  }
}, 10000); // 每10秒检查一次

第4步:使用React开发工具

# 安装React DevTools
npm install --save-dev @welldone-software/why-did-you-render

# 在项目入口添加
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

📋 五、项目中的检查清单

代码审查时检查这些:

✅ 每个 useEffect 都有返回清理函数吗?
✅ 定时器有 clearInterval/clearTimeout 吗?
✅ 事件监听有 removeEventListener 吗?
✅ 第三方库有 destroy/dispose 方法吗?
✅ 全局变量使用后置为 null 了吗?
✅ 大数组/对象使用后清空了吗?
✅ 组件卸载时取消了所有异步操作吗?

上线前必须测试:

1. 反复切换页面/路由 10次
2. 观察 Chrome 任务管理器内存变化
3. 看内存是稳定增长还是稳定
4. 用无痕模式测试(排除插件影响)

🚀 六、快速修复模板

情况1:修复异步请求泄漏

// 修复前
useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data));
}, []);

// 修复后
useEffect(() => {
  let isMounted = true; // 🔑 关键标志
  
  fetch('/api/data')
    .then(res => res.json())
    .then(data => {
      if (isMounted) { // 🔑 检查是否还在挂载
        setData(data);
      }
    });
  
  return () => {
    isMounted = false; // 🔑 组件卸载时设为false
  };
}, []);

情况2:修复定时器泄漏

// 修复前
useEffect(() => {
  setInterval(() => {
    updateCount();
  }, 1000);
}, []);

// 修复后
useEffect(() => {
  const timerId = setInterval(() => {
    updateCount();
  }, 1000);
  
  return () => clearInterval(timerId); // 🔑 清理定时器
}, []);

情况3:修复事件监听泄漏

// 修复前
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// 修复后
useEffect(() => {
  const handleResize = () => { /* ... */ };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize); // 🔑 移除监听
  };
}, []);

🎯 七、一句话总结

记住黄金法则:谁创建,谁清理。

  • 创建了定时器? → 记得 clearInterval
  • 添加了事件监听? → 记得 removeEventListener
  • 发起了请求? → 记得 abort
  • 用了第三方库? → 记得 destroy
  • 组件卸载了?useEffect 里记得 return 清理函数

🚨 实战练习

测试你的理解:找出下面代码的内存泄漏

// 找出3个内存泄漏点
function ProblemComponent() {
  useEffect(() => {
    // 泄漏点1: ______
    setInterval(() => {
      console.log('tick');
    }, 1000);
    
    // 泄漏点2: ______
    window.addEventListener('scroll', () => {
      console.log('scrolling');
    });
    
    // 泄漏点3: ______
    fetch('/api/data').then(data => {
      setState(data);
    });
  }, []);
  
  return <div>组件</div>;
}

答案:

  1. 定时器没清理 → 应该用 clearInterval
  2. 事件监听没移除 → 应该用 removeEventListener
  3. 请求没取消 → 应该用 AbortController

📞 遇到问题怎么办?

  1. 页面越来越卡 → 打开Chrome Memory拍快照对比
  2. 切换路由后内存不释放 → 检查useEffect清理函数
  3. 重复操作后崩溃 → 检查全局变量和闭包
  4. 第三方库有问题 → 检查是否有dispose方法

记住:90%的内存泄漏都是这3个问题:

  1. 定时器没清
  2. 监听没移除
  3. 请求没取消

解决方案就一句话:每个useEffect都要写return清理函数!

现在就去你的项目里,搜索 useEffect,检查有没有忘记写 return 的! 🚀

posted @ 2025-12-22 15:55  XiaoZhengTou  阅读(7)  评论(0)    收藏  举报