修复内存问题

185 阅读9分钟

学习如何使用Chrome和DevTools来查找影响页面性能的内存问题,包括内存泄漏、内存膨胀和频繁的垃圾收集。

 

摘要

  1. 使用Chrome任务管理器了解页面当前使用了多少内存。
  2. 利用时间轴录制功能可视化内存使用情况的变化。
  3. 使用堆快照来识别分离的DOM树(导致内存泄漏的常见原因之一)。
  4. 利用分配时间轴录制功能查找JS堆中何时分配了新内存。

 

概览:

在RAIL性能模型的精神指导下,你的性能努力应该集中在用户身上。

内存问题很重要,因为它们通常是用户能感知到的。用户可以通过以下方式感知内存问题:

  1. 页面性能随时间逐渐变差。这可能是内存泄漏的症状。内存泄漏是指页面中的错误导致页面随时间逐渐使用更多内存。
  2. 页面性能一直很差。这可能是内存膨胀的症状。内存膨胀是指页面使用的内存超过了实现最佳页面速度所需的内存。
  3. 页面性能延迟或似乎经常暂停。这可能是频繁进行垃圾收集的症状。垃圾收集是指浏览器回收内存的过程,浏览器决定何时进行垃圾收集。在收集期间,所有脚本执行都会暂停。因此,如果浏览器频繁进行垃圾收集,脚本执行就会频繁暂停。

 

内存膨胀:什么程度算是“太多”?

内存泄漏很容易定义。如果一个站点逐渐使用更多内存,那么就存在内存泄漏问题。但内存膨胀有点难以明确定义。什么程度的内存使用才算是“使用过多”呢?

这里没有硬性的数字,因为不同的设备和浏览器具有不同的能力。在高端智能手机上运行流畅的同一页面可能在低端智能手机上崩溃。

关键在于使用RAIL模型,并专注于用户。了解哪些设备在您的用户中较为流行,然后在这些设备上测试您的页面。如果用户的体验一直很差,那么页面可能超出了这些设备的内存能力。

 

 

Chrome任务管理器

使用Chrome任务管理器作为内存问题调查的起点。任务管理器是一个实时监视器,告诉您页面当前使用了多少内存。

按下Shift+Esc或转到Chrome主菜单,选择更多工具 > 任务管理器,以打开任务管理器。

打开任务管理器

image.png

右键单击任务管理器的表头并启用JavaScript内存。

image.png

启用JS内存

这两列向您提供有关页面内存使用情况的不同信息:

  1. 内存列代表本机内存。DOM节点存储在本机内存中。如果此值正在增加,则表示正在创建DOM节点。
  2. JavaScript内存列代表JS堆。此列包含两个值。您感兴趣的值是活动数字(括号中的数字)。活动数字表示您页面上可达对象使用了多少内存。如果此数字正在增加,要么是正在创建新对象,要么是现有对象在增长。

 

 

使用性能录制可视化内存泄漏

您还可以使用性能面板作为调查的另一个起点。性能面板帮助您随时间可视化页面的内存使用情况。

  1. 在DevTools中打开性能面板。
  2. 启用"内存"复选框。

进行录制。

  1. 提示:最好的做法是在录制开始和结束时进行强制垃圾收集。在录制时点击"收集垃圾"按钮(强制垃圾收集按钮)来强制进行垃圾收集。

为了演示性能内存录制,考虑下面的代码:

 

每次按下代码中引用的按钮时,会向文档主体附加一万个div节点,并将一个包含一百万个"x"字符的字符串推送到"x"数组中。运行这段代码会生成一个类似下面截图的时间轴录制:

在这种情况下,时间轴录制将显示随着时间的推移内存的使用情况,特别是在按钮被按下的时候。您可以观察到内存的急剧增加,这可能是内存泄漏的迹象。在分析这样的录制时,您可以尝试识别内存泄漏或不必要的内存分配,并采取适当的措施来改进代码的内存管理。

 

首先,对用户界面的解释。概览窗格中(位于NET下方)的HEAP图表示JS堆。在概览窗格下方是计数器窗格。在这里,您可以看到内存使用情况按JS堆(与概览窗格中的HEAP图相同)、文档、DOM节点、监听器和GPU内存进行了分解。禁用复选框会使其在图表中隐藏。

现在,对比代码和截图的分析。如果查看节点计数器(绿色图),可以看到它与代码清晰地匹配。节点计数以离散步骤增加。您可以推断每次节点计数增加都是对grow()的调用。JS堆图(蓝色图)则没有那么直观。按照最佳实践,第一个下降实际上是强制垃圾收集(通过按下收集垃圾按钮实现)。随着录制的进行,可以看到JS堆大小出现峰值。这是自然且预期的:JavaScript代码在每次按钮点击时都会创建DOM节点,并在创建包含一百万字符的字符串时执行大量工作。关键的是JS堆的结束大小高于开始大小(这里的“开始”是强制垃圾收集之后的点)。在现实世界中,如果看到这种JS堆大小或节点大小逐渐增加的模式,可能意味着存在内存泄漏。

 

使用堆快照发现分离的DOM树内存泄漏

DOM节点只有在页面的DOM树或JavaScript代码中没有对其的引用时才能被垃圾收集。当节点从DOM树中移除但仍然被某些JavaScript引用时,该节点被称为“分离”。分离的DOM节点是内存泄漏的常见原因。本节将教您如何使用DevTools的堆分析器来识别分离的节点。

这里有一个分离的DOM节点的简单示例:

单击代码中引用的按钮会创建一个带有十个li子元素的ul节点。这些节点由代码引用,但在DOM树中并不存在,因此它们是分离的。

堆快照是识别分离节点的一种方法。正如其名称所示,堆快照在快照时显示您页面的JS对象和DOM节点在内存中的分布方式。

要创建快照,请打开DevTools并转到Memory面板,选择Heap Snapshot单选按钮,然后按下Take Snapshot按钮。

 

  1. 快照可能需要一些时间来处理和加载。完成后,请在左侧面板(命名为HEAP SNAPSHOTS)中选择它。

在Class过滤文本框中键入“Detached”以搜索分离的DOM树。

展开小箭头以调查游离的树。

被黄色标记的节点从JavaScript代码中直接引用。被红色标记的节点没有直接引用。它们仅存活,因为它们是黄色节点树的一部分。一般来说,您希望专注于黄色节点。修复代码,使黄色节点的存活时间不会超过必要的时间,并且清除黄色节点树的红色节点。

 

单击黄色节点以进一步调查。在对象面板中,您可以看到有关引用它的代码的更多信息。例如,在下面的截图中,您可以看到detachedTree变量引用了该节点。要修复此特定内存泄漏,您将研究使用detachedTree的代码,并确保在不再需要时删除对该节点的引用。

使用分配时间轴识别JS堆内存泄漏

分配时间轴是另一个工具,可帮助您追踪JS堆中的内存泄漏。

为演示分配时间轴,请考虑以下代码:

每次按下代码中引用的按钮时,都会将一个包含一百万个字符的字符串添加到x数组中。

要记录分配时间轴,请打开DevTools,转到Profiles面板,选择Record Allocation Timeline单选按钮,按下Start按钮,执行您认为会导致内存泄漏的操作,然后在完成时按下停止录制按钮。

在录制时,请注意是否有任何蓝色条在分配时间轴上显示,就像下面的截图中一样。

这些蓝色条表示新的内存分配。这些新的内存分配是可能的内存泄漏候选项。您可以放大柱形图,以在Constructor窗格中仅显示在指定时间范围内分配的对象。

展开对象并单击其值,以在对象面板中查看有关其更多详细信息。例如,在下面的截图中,通过查看新分配的对象的详细信息,您可以看到它是在Window范围内分配给x变量的。

通过函数调查内存分配

使用Memory面板中的Allocation Sampling类型查看JavaScript函数的内存分配情况。

  1. 选择Allocation Sampling单选按钮。如果页面上有一个worker,您可以使用Start按钮旁边的下拉菜单选择它作为分析目标。
  2. 按下Start按钮。
  3. 执行要调查的页面上的操作。
  4. 完成所有操作后按下Stop按钮。
  5. DevTools将向您显示按函数进行的内存分配的详细信息。默认视图是Heavy(Bottom Up),显示在顶部分配最多内存的函数。

 

发现频繁的垃圾收集

如果您的页面似乎经常出现暂停,那么可能存在垃圾收集问题。

您可以使用Chrome任务管理器或时间轴内存录制来发现频繁的垃圾收集。在任务管理器中,Memory或JavaScript Memory值的频繁上升和下降表示频繁的垃圾收集。在时间轴录制中,JS堆或节点计数图的频繁上升和下降表示频繁的垃圾收集。

一旦确定了问题,您可以使用分配时间轴录制来查找内存分配的位置以及哪些函数导致了这些分配。