浏览器内存泄漏排查指南

1、setTimeout执行原理

使用setInterval/setTimeOut遇到的坑 - 掘金

浏览器内存泄漏排查指南_第1张图片

2、Chrome自带的Performance工具

当我们怀疑页面发生了内存泄漏的时候,可以先用Performance录制一段时间内页面的内存变化。

  • 点击开始录制
  • 执行可能引起内存泄漏的操作
  • 点击停止录制

浏览器内存泄漏排查指南_第2张图片

如果录制结束后,看到内存的下限在不断升高的话,你就要注意了 —— 这里有可能发生了内存泄漏。

除了内存增长曲线,Nodes(Dom节点数曲线)、Document曲线以及Listener曲线也同样值得关注,有时候它们对内存问题的定位也很有帮助。

你怀疑发生了内存泄漏的时候,你就可以用Memory面板来进一步定位泄漏的源头了。

3、Chrome自带的Memory工具

从Memory的主界面开始,点击左上角的圆点就可以记录下当前的堆内存快照(heap snapshot)了。

浏览器内存泄漏排查指南_第3张图片

  1. 打开DevTools, 切换至Memory面板
  2. 先记录一个堆内存快照
  3. 执行可能发生泄漏的操作
  4. 再记录一个堆内存快照
  5. 重复执行多几遍步骤3,4
  6. 选择最后一个堆内存快照,找到顶栏的“All objects”, 切换至”Objects allocated between snapshots 1 and 2”(也可以对2,3执行同样的操作)

切换后,你就能看到两个快照之间新生成的对象。你可以选择其中一项点开,看看它的retaining tree里面保留了哪些对象没有释放。

4、经验说明

4.1 为什么我的内存快照记录下来之后看不懂,还出现了很多奇怪的变量

除去教程里demo代码比较简单之外,提前准备好一个合理的debug环境也是很重要的。这里我列举了4点个人觉得对debug内存问题很有帮助的措施:

1. 尽量使用没有混淆的代码:打包后的代码往往经过了混淆和压缩,在生产环境上这是必要的,但在debug时却会成为我们的绊脚石,不便于阅读。

2. 排查问题时使用production模式编译出来的代码:Dev模式下往往会开启一些方便开发的特性,例如热更新等。但它们可能会占用一部分的内存,影响到内存问题的排查,所以建议还是使用production模式编译出来的代码进行问题排查。

3. 屏蔽所有浏览器插件:屏蔽浏览器插件最快的方式就是打开无痕窗口。浏览器插件给我们带来很多便利,但插件注入的额外逻辑有时也会影响内存问题的排查。例如vue-devtools会记录下每一个vuex mutaions,导致内存无法释放。

4. 在现场打内存快照,便于跳转到源代码所在行:尽管devTools记录下来的内存快照文件可以单独加载展示,但还是建议在记录下内存快照的时候“趁热”分析,因为这时还能从retaining tree上跳转到代码所在行,有时候对定位问题也很有帮助。

4.2 快照里有一些“Detached DOM tree”,是什么意思

一个DOM节点只有在没有被页面的DOM树或者Javascript引用时,才会被垃圾回收。当一个节点处于“detached”状态,表示它已经不在DOM树上了,但Javascript仍旧对它有引用,所以暂时没有被回收。通常,Detached DOM tree往往会造成内存泄漏,我们可以重点分析这部分的数据。

4.3 常见的内存泄漏场景有哪些

  1. console导致的内存泄漏 因为打印后的对象需要支持在控制台上查看,所以传递给console.log方法的对象是不能被垃圾回收的。我们需要避免在生产环境用console打印对象。
  2. 框架配合第三方库使用时,没有及时执行销毁 这点可以参考vue cookbook里的例子:避免内存泄漏 — Vue.js 中文文档
  3. 被遗忘的定时器 例如在组件初始化的时候设置了setInterval,那么在组件销毁之前记得调用clearInterval方法取消定时器。
  4. 没有正确移除事件监听器(各种EventBus, dom事件监听等) 这应该是最容易犯的一个错误,无论新手老手都有可能栽在这里。
    特征:performance里,监听器数量会持续上升

浏览器内存泄漏排查指南_第4张图片

补充说明:

尽管大部分同学都会有主动移除监听器的观念,但如果姿势不对,可能依旧会造成内存泄漏。下面是一个真实案例:

// 版本一
mounted() {
    window.addEventListener('resize', debounce(this.handleWidthChange, 100))
},
beforeDestroy() {
    window.removeEventListener('resize', debounce(this.handleWidthChange, 100)) 
}

乍一看好像写的还不错,有及时移除监听器,对resize这种频繁触发的事件也加了debounce处理。但其实这段代码就导致了内存泄漏:每次调用debounce(this.handleWidthChange, 100)时, 其实都会返回一个新的函数,导致addEventListener和 removeEventListener方法传入的回调函数已经不是同一个回调函数,监听器没有被正确移除,内存泄漏。 




浏览器内存泄漏排查指南_第5张图片

 修改后的代码如下:

data() {
    return {
        debounceWidthChange: null
    }
},
mounted() {
    this.debounceWidthChange = debounce(this.handleWidthChange, 100)
    window.addEventListener('resize', this.debounceWidthChange)
},
beforeDestroy() {
    window.removeEventListener('resize', this.debounceWidthChange)  
}

5、总结

简单总结一下排查内存泄漏的常见流程:

  1.  用performance面板记录操作一段时间内的内存变化,找出可能发生内存泄漏的操作。
  2.  用“three snapshot”技巧,记录下发生泄漏前后的内存快照。
  3.  用comparison视图对泄漏前后的内存快照进行比较,找出泄漏的对象。
  4. 重点关注 Vue Component, Detached HTMLDivElement等Constructor。

备注:

 手把手教你排查Javascript内存泄漏 - 知乎

你可能感兴趣的:(前端,javascript,开发语言)