ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JS内存溢出怎么排查,从现象定位到代码根因

JS内存溢出怎么排查,从现象定位到代码根因 说实话前端开发里最让人头皮发麻的问题内存溢出Memory Leak绝对排前三。因为它不像报错那样会直接给你一个红叉和堆栈信息而是悄无声息地让你的页面越用越卡最后直接崩掉。我记得有一次公司的一个后台管理系统用户反馈说“用了一上午就卡得点不动了”。我们一开始还以为是接口慢查了半天发现接口响应都正常。后来打开任务管理器一看好家伙标签页内存占用快2个G了。这就是典型的内存泄漏。今天咱们就来聊聊当页面出现内存问题时应该怎么一步步去排查、定位、修复。不讲虚的直接上实操。1. 先判断真的是内存溢出吗有时候页面卡顿不一定是内存问题可能是CPU占用高、网络慢、或者DOM节点太多导致的渲染卡顿。所以第一步得确认是不是内存泄漏。最直观的方法就是打开浏览器的任务管理器Chrome里按Shift Esc找到你的标签页观察两个指标内存占用如果这个数值一直在增长没有回落说明有内存泄漏CPU占用如果CPU一直居高不下可能是密集计算或者频繁重绘另一个方法是打开开发者工具F12-Performance性能面板录制一段操作然后看内存曲线Memory。正常的内存曲线应该是有升有降的因为垃圾回收会定时清理如果曲线是一路向上不带回头的那基本可以确定有内存泄漏。2. 抓“元凶”Heap Snapshot 快照对比法确认有内存泄漏之后接下来要找出是哪个对象一直在占用内存。这时候要用到Memory内存面板里的Heap Snapshot堆快照。操作步骤第一步打开页面做一次初始操作比如刚加载完拍第一个快照Snapshot 1。第二步重复执行某个你认为可能有问题的操作比如打开一个弹窗再关闭反复10次。第三步拍第二个快照Snapshot 2然后切换到Comparison对比视图选择跟 Snapshot 1 对比。这时候你会看到一个列表里面列出了“新增的对象”和“仍然存活的对象”。重点关注那些#New数量多、且没有被释放的对象。比如你发现有个Window对象或EventEmitter实例一直没被释放那很可能就是事件监听没移除。如果你发现一堆HTMLDivElement或HTMLSpanElement没被释放那就是DOM节点没有被正确回收。3. 实战案例一定时器引发的血案这是最经典的内存泄漏场景之一。看这段代码export default { data() { return { count: 0 }; }, mounted() { this.timer setInterval(() { this.count; console.log(this.count); }, 1000); }, // 忘记在 beforeUnmount 里清除定时器 };每次进入这个页面都会创建一个新的定时器。但离开页面时定时器没有被清除它仍然持有对组件实例this的引用。组件实例无法被垃圾回收里面的数据、DOM引用全被“锁”住了。页面进出个十几次内存里就攒了十几个“僵尸组件”。排查方法在 Heap Snapshot 里搜索setInterval或者你的组件名看看是不是有多个实例同时存在。修复方法beforeUnmount() { if (this.timer) { clearInterval(this.timer); this.timer null; } }如果用的是 Vue3 的 setup 语法就放在onUnmounted里。4. 实战案例二事件监听没有移除事件监听也是重灾区。特别是在mounted里绑定了全局事件然后在beforeUnmount里忘了移除。mounted() { window.addEventListener(resize, this.handleResize); document.addEventListener(scroll, this.handleScroll); }, // 忘了在 beforeUnmount 里 removeEventListener当组件销毁后这些事件监听依然挂在window或document上而且它们的回调函数handleResize绑定了组件的this导致整个组件实例无法被回收。排查方法在 Heap Snapshot 里搜索handleResize或EventListener看是否有被卸载组件的事件监听仍然存在。修复方法成对出现成对消失。beforeUnmount() { window.removeEventListener(resize, this.handleResize); document.removeEventListener(scroll, this.handleScroll); }还有一个容易忽略的点用addEventListener绑定了DOM事件的匿名函数是无法移除的所以一定要用具名函数。5. 实战案例三闭包导致的大对象无法释放闭包是个好东西但用不好也会造成内存泄漏。function createHeavyObject() { const bigData new Array(1000000).fill(*); // 大数组 return function() { // 这个函数虽然没有直接使用 bigData // 但因为闭包的作用bigData 一直活在内存里 console.log(hello); }; } const fn createHeavyObject(); // fn 一直存在bigData 就永远释放不了更隐蔽的版本是 Vue 的 computed 或 watch 里使用了外部的大对象const bigData ref(new Array(1000000).fill(*)); const someComputed computed(() { // 这里如果返回了 bigData 的一部分并且被长期引用 return bigData.value.slice(0, 100); }); // 如果不小心把 someComputed 赋值给了一个全局变量那整个 bigData 都释放不了排查方法在 Heap Snapshot 里按“Retained Size保留大小”排序看哪些对象占用的内存最大然后顺着引用链往上找看看是谁在引用它。修复方法确保不需要的时候把引用置为null。或者在闭包中只保留必要的数据不要“连带”一大堆无关的东西。6. 实战案例四DOM 节点引用未清理当你用变量缓存了一个DOM节点但该节点被从页面上移除了你的变量仍然指向它导致这个DOM节点无法被垃圾回收。let cachedDiv document.getElementById(cached); function removeDiv() { const div document.getElementById(cached); div.parentNode.removeChild(div); // 但 cachedDiv 变量还指向这个被移除的 div // 这个 DOM 节点就永远停留在内存里了 }排查方法在 Heap Snapshot 里搜索Detached分离的关键字可以找到所有已经从DOM树中移除但还被JS引用的节点。修复方法移除DOM节点时顺手把JS引用也置空。function removeDiv() { const div document.getElementById(cached); div.parentNode.removeChild(div); cachedDiv null; // 手动释放 }7. Chrome DevTools 的几个实用小技巧① 录制性能并查看内存曲线在 Performance 面板录制一段操作勾选Memory选项。录制结束后看底部的Heap曲线如果曲线在垃圾回收图中会有小三角标记后没有明显下降说明有内存泄漏。② 使用 Allocation Timeline分配时间线在 Memory 面板选择Allocation instrumentation on timeline然后开始录制它会实时显示内存分配的情况。如果某个操作后内存分配量突然暴增且没有回落那就是问题点。③ 手动触发垃圾回收在 Memory 面板左侧有个小垃圾桶图标点击可以手动触发垃圾回收。在拍快照前点一下可以排除一些“暂存”对象的干扰让快照更干净。8. 预防大于排查与其等到内存溢出再排查不如在编码时就养成好习惯场景正确做法定时器 / 间隔器mounted里创建beforeUnmount里清除事件监听全局绑定和解绑成对出现第三方库订阅调用subscribe后记得存下unsubscribe并调用大数组 / 大对象用完后手动置null释放引用DOM 节点引用节点移除后变量也置null请求fetch/axios用AbortController在组件销毁时取消未完成的请求总结一下排查流程观察现象页面越来越卡内存占用持续攀升确认问题用 Chrome 任务管理器或 Performance 面板确认内存泄漏定位对象用 Heap Snapshot 对比法找到泄漏的对象类型追溯代码查看对象引用链找到是谁在持有它修复根因按上面表格里的方式清理定时器、监听、引用等验证修复重新跑一遍操作确认内存曲线恢复正常排查内存泄漏确实是个细活有时候一个疏忽就能让你折腾一下午。但一旦掌握了这套方法大部分内存问题都能迎刃而解。如果你在实际项目中遇到过更奇葩的内存泄漏案例欢迎在评论区分享出来大家一起涨姿势。觉得有用的话点个收藏下次排查的时候直接翻出来对照着来~
返回列表