前端内存泄漏排查实录,页面越用越卡如何定位 之前维护一个内部后台管理系统Vue2 开发的 SPA 项目测试反馈系统操作久了页面卡顿切换菜单之后交互明显迟钝。一开始我以为是表格渲染大量 DOM 导致做了虚拟列表优化问题依旧存在。打开浏览器任务管理器观察内存每切换一次页面内存只涨不跌几乎没有回收很明显存在内存泄漏。前端内存泄漏本质就是已经不再需要的对象因为还有引用存在GC 垃圾回收器无法把它释放掉。单页应用路由跳转组件销毁之后本该释放的内存一直堆积使用时间越久内存占用越高页面就会越来越卡。很多同学知道内存泄漏这个概念但不知道哪些写法会造成泄漏也不知道怎么去排查。下面列出项目中遇到的高频泄漏场景附带错误代码、问题说明以及修复方案。场景 1定时器未清除组件销毁后依旧在运行这是最常见的内存泄漏场景。组件里面写了setInterval、setTimeout组件卸载的时候忘记清除定时器。定时器回调持有组件实例、DOM 的引用组件销毁也无法被 GC 回收。Vue 错误示例代码template div定时器测试组件/div /template script export default { mounted() { // 开启轮询 this.timer setInterval(() { console.log(轮询请求数据); }, 1000); }, beforeDestroy() { // 忘记清除定时器泄漏源头 } } /script组件路由跳转卸载之后定时器依旧在后台执行组件实例得不到释放内存持续上涨。修复方案组件销毁钩子里面清除定时器script export default { data(){ return { timer: null } }, mounted() { this.timer setInterval(() { console.log(轮询请求数据); }, 1000); }, beforeDestroy() { // 销毁时关闭定时器 if(this.timer){ clearInterval(this.timer); this.timer null; } } } /script补充setTimeout 也需要手动清除不要觉得只执行一次就无所谓如果组件很快卸载定时器还没执行同样会造成引用残留。场景 2全局事件监听没有解绑给 window、document 添加事件监听组件销毁的时候没有 removeEventListener。全局对象一直持有回调函数引用组件内存无法释放。错误写法export default { mounted(){ window.addEventListener(resize, (){ console.log(窗口大小变化); }) }, beforeDestroy(){ // 没有移除resize监听内存泄漏 } }这里有一个坑匿名回调函数无法移除removeEventListener 必须传入和 addEventListener 完全相同的函数引用。正确写法export default { methods:{ handleResize(){ console.log(窗口大小变化); } }, mounted(){ window.addEventListener(resize, this.handleResize) }, beforeDestroy(){ window.removeEventListener(resize, this.handleResize) } }场景 3闭包意外保留 DOM 节点引用闭包是一把双刃剑合理使用很方便但一不小心会把已经移除的 DOM 节点保存下来。DOM 元素从页面移除JS 变量依旧持有它的引用DOM 内存不会释放。原生 JS 复现代码div idapp div idbox测试DOM节点/div button onclickremoveDom()删除节点/button /div script let domCache null; function removeDom(){ const boxDom document.getElementById(box); // 闭包缓存DOM引用 domCache boxDom; // 从页面移除DOM boxDom.remove(); // DOM已经不在页面上但是domCache还持有引用无法GC回收 } /scriptDOM 已经从页面删除但是 JS 变量还保存着 DOM 对象内存无法释放。业务中经常出现在全局缓存、store 缓存 DOM 对象的场景。 修复不再使用的时候手动置为 null切断引用。domCache null;场景 4Vue/React 全局事件总线忘记取消订阅很多老项目会用 EventBus 做组件通信组件监听事件组件销毁不取消 on 订阅事件总线一直保存回调函数组件实例无法回收。Vue2 EventBus 错误示例// 组件内部 mounted(){ this.$bus.$on(refreshData, (){ this.loadData() }) }, beforeDestroy(){ // 没有$off取消订阅内存泄漏 }修复组件销毁取消监听mounted(){ this.$bus.$on(refreshData, this.loadData) }, beforeDestroy(){ this.$bus.$off(refreshData,this.loadData) }使用 Chrome DevTools 定位内存泄漏实操光知道坑还不够线上复杂业务很难一眼看出哪里泄漏需要借助浏览器工具定位。打开开发者工具 Memory 面板点击录制快照 Take snapshot操作页面复现问题反复切换路由再次拍快照对比前后两张快照内存大小通过 Comparison 对比筛选 Detached DOM node脱离文档流的 DOM 节点找到没有被回收的对象查看 Retainers 引用链看是哪个变量还持有该对象定位泄漏代码。小技巧操作页面之后手动点击垃圾回收按钮再拍快照排除正常未回收干扰。容易混淆卡顿不等于内存泄漏有一点需要区分页面卡顿不一定就是内存泄漏。内存持续上涨页面关闭组件之后内存不回落 → 大概率内存泄漏只是渲染大量 DOM、复杂计算导致卡顿内存可以正常回落不属于泄漏。开发日常预防小习惯定时器、延时器组件销毁一定要清除window、document 全局事件监听就要记得解绑EventBus、自定义事件组件卸载取消订阅不要把 DOM 对象存到全局变量、状态管理里面闭包谨慎使用避免无意长期持有大对象开发阶段偶尔打开 Memory 面板简单观察内存曲线提前发现隐患。写在最后前端内存泄漏属于隐性 bug功能测试很难发现只有长时间操作系统才会暴露。很多业务开发容易忽略这块等到用户反馈页面卡顿的时候问题已经积累很久。排查的时候不要上来就盲改代码先用 Memory 快照工具确认是否真的发生泄漏顺着引用链找到源头再针对性修复。平时写代码养成好习惯提前规避大部分泄漏问题远比线上出问题再排查要高效。