了解如何使用 Chrome 和开发者工具查找影响页面性能的内存问题,包括内存泄漏、内存膨胀和频繁的垃圾回收。
摘要
- 使用 Chrome 任务管理器了解您的网页目前使用了多少内存。
- 使用时间轴记录直观呈现一段时间内的内存用量。
- 使用堆快照确定已分离的 DOM 树(内存泄漏的常见原因)。
- 通过分配时间线记录了解新内存在 JS 堆中的分配时间。
概览
本着 RAIL 性能模型的精神,性能工作的重点应该是用户。
内存问题很重要,因为用户通常可以察觉到这些问题。用户可以通过以下方式感知内存问题:
- 网页的性能随着时间的推移逐渐下降。这可能是内存泄漏的症状。内存泄漏是指页面中的 bug 导致页面在一段时间内使用的内存越来越多。
- 网页性能一直很糟糕。这可能是内存膨胀的症状。内存膨胀是指页面为实现最佳速度而使用的内存超过所需内存。
- 网页呈现延迟现象或经常暂停。这可能是垃圾回收频繁的表现。垃圾回收是指浏览器收回内存。浏览器会决定何时进行此操作。回收期间,所有脚本执行都将暂停。因此,如果浏览器经常进行垃圾回收,脚本执行就会被频繁暂停。
内存膨胀:什么是“过多”?
内存泄漏很容易定义。如果网站使用的内存越来越多,则表示发生内存泄漏。但内存膨胀更难以确定。什么情况才算是“使用过多内存”?
这里不存在硬性数字,因为不同的设备和浏览器具有不同的功能。 在高端智能手机上能够流畅运行的相同网页在低端智能手机上也可能会崩溃。
此处的关键是使用 RAIL 模型并以用户为中心。了解哪些设备深受用户欢迎,然后在这些设备上测试您的网页。如果体验一直很糟糕,则页面可能会超出这些设备的内存能力。
使用 Chrome 任务管理器实时监控内存使用情况
您可以使用 Chrome 任务管理器着手调查内存问题。任务管理器是一个实时监视器,可以告知您页面当前使用的内存量。
按 Shift+Esc 或转到 Chrome 主菜单,然后依次选择更多工具 > 任务管理器,以打开任务管理器。
右键点击任务管理器的表格标题,然后启用 JavaScript 内存。
通过以下两列,您可以了解网页的内存使用方式:
- Memory 列表示原生内存。DOM 节点存储在原生内存中。如果此值逐渐增大,系统会创建 DOM 节点。
- JavaScript Memory 列表示 JS 堆。此列包含两个值。您感兴趣的值是实时数字(括号中的数字)。实时数字表示您的页面上的可到达对象正在使用的内存量。如果此数字不断增加,要么是正在创建新对象,要么是现有对象在增长。
使用性能记录直观呈现内存泄漏
您也可以使用“效果”面板作为调查的起点。“性能”面板有助于您直观呈现页面在一段时间内的内存使用情况。
- 在开发者工具上打开 Performance 面板。
- 选中 Memory 复选框。
- 录制。
如需演示性能内存记录,请考虑使用下面的代码:
var x = [];
function grow() {
for (var i = 0; i < 10000; i++) {
document.body.appendChild(document.createElement('div'));
}
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
每次按下代码中引用的按钮时,都会向文档正文附加一万个 div
节点,并将一百万个 x
字符的字符串推送到 x
数组。运行此代码会生成如以下屏幕截图所示的 Timeline 记录:
首先是界面说明。Overview 窗格中的 HEAP 图表(NET 下方)表示 JS 堆。概览窗格下方是计数器窗格。在这里,您可以查看按 JS 堆(与 Overview 窗格中的 HEAP 图表相同)、文档、DOM 节点、监听器和 GPU 内存细分的内存用量。取消选中复选框可在图表中隐藏它。
接下来,分析代码与屏幕截图。如果您查看节点计数器(绿色图表),就会发现它与代码完全匹配。节点数量以离散步长的方式增加。您可以假定节点计数的每次增加都是对 grow()
的调用。JS 堆图表(蓝色图表)就没有那么简单了。按照最佳实践,第一次低谷实际上是强制垃圾回收(通过按回收垃圾按钮实现)。随着记录的进度,您可以看到 JS 堆大小急剧增加。这是正常现象,也是在意料之中:JavaScript 代码会在每次点击按钮时创建 DOM 节点,并在创建包含 100 万个字符的字符串时完成大量工作。这里的关键是,JS 堆的结束时间高于它的开始时间(这里的“开始”是指强制垃圾回收之后的时间点)。在实际使用中,如果您看到这种增加 JS 堆大小或节点大小的模式,则可能意味着发生内存泄漏。
使用堆快照发现已分离的 DOM 树的内存泄漏
只有当网页的 DOM 树或 JavaScript 代码不再引用某个 DOM 节点时,系统才会对这个节点进行垃圾回收。如果某个节点已从 DOM 树中移除,但某些 JavaScript 仍会引用该节点,我们称该节点已“分离”。已分离的 DOM 节点是导致内存泄漏的常见原因。本部分介绍了如何使用开发者工具的堆性能分析器识别已分离的节点。
下面是一个已分离的 DOM 节点的简单示例。
var detachedTree;
function create() {
var ul = document.createElement('ul');
for (var i = 0; i < 10; i++) {
var li = document.createElement('li');
ul.appendChild(li);
}
detachedTree = ul;
}
document.getElementById('create').addEventListener('click', create);
点击代码中引用的按钮可创建一个包含 10 个 li
子级的 ul
节点。这些节点由代码引用,但不存在于 DOM 树中,因此它们已分离。
堆快照是识别已分离节点的一种方式。顾名思义,堆快照可显示快照发生时内存在您页面的 JS 对象和 DOM 节点之间的分配情况。
如需创建快照,请打开开发者工具并转到 Memory 面板,选择堆快照单选按钮,然后按截取快照按钮。
快照可能需要一些时间来处理和加载。完成后,从左侧面板中(名为 HEAP SNAPSHOTS)将其选中。
在 Class filter 文本框中输入 Detached
,以搜索已分离的 DOM 树。
展开三角符号以调查分离的树。
以黄色突出显示的节点具有 JavaScript 代码对它们的直接引用。以红色突出显示的节点没有直接引用。只有属于黄色节点树时,它们才处于活动状态。一般而言,您需要将注意力集中在黄色节点上。请修正您的代码,使黄色节点的活跃时间不超过需要的时间,同时您还要移除属于黄色节点树的红色节点。
点击黄色节点可对其进行进一步调查。在 Objects 窗格中,您可以详细了解引用该窗格的代码。例如,在下面的屏幕截图中,您可以看到 detachedTree
变量正在引用该节点。如需修复此特定内存泄漏问题,您需要研究使用 detachedTree
的代码,并确保在不再需要时移除对节点的引用。
使用分配时间轴识别 JS 堆内存泄漏
分配时间轴是另一种工具,可以帮助您跟踪 JS 堆中的内存泄漏。
要显示分配时间线,请考虑使用以下代码:
var x = [];
function grow() {
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
每次推送代码中引用的按钮时,都会向 x
数组添加一百万个字符的字符串。
如需录制分配时间轴,请打开开发者工具,转到 Profiles 面板,选择 Record Allocation Timeline 单选按钮,按 Start 按钮,执行您怀疑会导致内存泄漏的操作,然后在完成后按 Stop recording 按钮 ()。
在记录时,请注意分配时间轴上是否显示任何蓝色竖条(如下面的屏幕截图所示)。
这些蓝条表示新的内存分配。这些新的内存分配可能会导致内存泄漏。您可以缩放条形,以过滤 Constructor 窗格,使其仅显示在指定时间范围内分配的对象。
展开对象并点击其值,即可在 Object 窗格中查看其更多详细信息。例如,在下面的屏幕截图中,通过查看新分配的对象的详细信息,您可以看到它已被分配给 Window
范围内的 x
变量。
按函数调查内存分配
使用 Memory 面板中的 Allocation Sampling 类型可按 JavaScript 函数查看内存分配。
- 选择分配采样单选按钮。如果页面上存在工作器,您可以使用 Start 按钮旁边的下拉菜单将其选为分析目标。
- 按 Start 按钮。
- 在您想要调查的网页上执行操作。
- 完成所有操作后,按停止按钮。
开发者工具会按函数显示内存分配明细。默认视图为 Heavy (Bottom Up),显示分配了最多内存的函数。
发现频繁的垃圾回收
如果您的网页似乎经常暂停,则可能存在垃圾回收问题。
您可以使用 Chrome 任务管理器或时间轴内存记录来发现频繁的垃圾回收。在任务管理器中,Memory 或 JavaScript Memory 值频繁上升和下降表示存在频繁的垃圾回收。在时间轴记录中,JS 堆或节点计数图表频繁上升和下降表示存在频繁的垃圾回收。
确定问题后,您可以使用分配时间轴记录找出内存正在分配的位置以及哪些函数导致了分配。