了解如何使用 Chrome 和开发者工具查找影响网页性能的内存问题,包括内存泄漏、内存膨胀和频繁的垃圾回收。
摘要
- 使用 Chrome 任务管理器了解网页占用的内存量。
- 通过时间轴记录直观呈现一段时间内的内存用量。
- 使用堆快照识别分离的 DOM 树(内存泄漏的常见原因)。
- 通过分配时间轴记录,了解何时在 JS 堆中分配了新内存。
- 识别由 JavaScript 引用保留的已分离元素。
概览
本着 RAIL 性能模型的精神,您在性能方面所做努力的重点应该是用户。
内存问题很重要,因为用户通常可以感知到这些问题。用户可以通过以下方式感知内存问题:
- 网页性能随着时间的推移而逐渐变差。这可能是内存泄漏的症状。内存泄漏是指页面中的 bug 导致页面随着时间的推移逐渐使用越来越多的内存。
- 网页性能始终不佳。这可能是内存膨胀的症状。内存膨胀是指网页使用的内存超出了实现最佳网页速度所需的内存。
- 网页性能延迟或似乎经常暂停。这可能是频繁进行垃圾回收的症状。垃圾回收是指浏览器回收内存。浏览器会决定何时发生这种情况。在收集期间,所有脚本执行都会暂停。因此,如果浏览器频繁进行垃圾回收,脚本执行就会频繁暂停。
内存膨胀:多少才算“太多”?
内存泄漏很容易定义。如果网站使用的内存越来越多,则说明存在泄漏。但内存膨胀问题更难确定。什么情况属于“占用过多内存”?
这里没有确切的数字,因为不同的设备和浏览器具有不同的功能。 同一个网页在高端智能手机上运行流畅,但在低端智能手机上可能会崩溃。
这里的关键是使用 RAIL 模型并专注于用户。了解用户常用的设备,然后在这些设备上测试您的网页。如果体验始终不佳,则该网页可能超出了这些设备的内存容量。
使用 Chrome 任务管理器实时监控内存用量
首先使用 Chrome 任务管理器调查内存问题。任务管理器是一个实时监控工具,可让您了解网页正在使用的内存量。
按 Shift+Esc 或前往 Chrome 主菜单,然后依次选择更多工具 > 任务管理器,打开任务管理器。

右键点击任务管理器的表头,然后启用 JavaScript 内存。

这两列会从不同角度反映网页的内存使用情况:
- 内存占用列表示操作系统内存。DOM 节点存储在操作系统内存中。如果此值不断增加,则表示正在创建 DOM 节点。
JavaScript 内存列表示 JS 堆。此列包含两个值。您感兴趣的值是实时数字(括号中的数字)。实时数字表示页面上可访问的对象正在使用的内存量。如果此数字在增加,则表示正在创建新对象,或者现有对象正在增长。

通过 Performance 录制直观呈现内存泄漏
您还可以使用性能面板作为调查的另一个切入点。通过“Performance”面板,您可以直观地了解页面在一段时间内的内存用量。
- 在开发者工具中打开性能面板。
- 选中内存复选框。
- 录制内容。
为了演示性能内存记录,请考虑以下代码:
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);
每次按下代码中引用的按钮时,系统都会将 1 万个 div 节点附加到文档正文,并将一个包含 100 万个 x 字符的字符串推送到 x 数组。
运行此代码会生成一个时间轴记录,如以下屏幕截图所示:

首先,我们来了解一下界面。概览窗格(NET 下方)中的 HEAP 图表表示 JS 堆。概览窗格下方是计数器窗格。在这里,您可以查看按 JS 堆(与概览窗格中的 HEAP 图表相同)、文档、DOM 节点、监听器和 GPU 内存细分的内存用量。停用复选框会将其从图表中隐藏。
现在,我们来分析一下代码与屏幕截图的对比情况。如果您查看节点计数器(绿色图表),会发现它与代码完全一致。节点数量会以离散步长增加。您可以假设节点数量的每次增加都是对 grow() 的调用。JS 堆图(蓝色图)则不那么直观。根据最佳实践,第一个下降实际上是强制垃圾回收(通过按回收垃圾按钮实现)。随着记录的进行,您可以看到 JS 堆大小激增。这是自然而然且符合预期的:JavaScript 代码会在每次点击按钮时创建 DOM 节点,并在创建包含 100 万个字符的字符串时执行大量工作。这里的关键在于,JS 堆的结束位置高于开始位置(这里的“开始”是指强制垃圾回收之后的点)。在现实世界中,如果您看到 JS 堆大小或节点大小不断增加,则可能意味着存在内存泄漏。
使用堆快照发现分离的 DOM 树内存泄漏
只有在网页的 DOM 树或 JavaScript 代码中没有对 DOM 节点的引用时,才能对该节点进行垃圾回收。当节点从 DOM 树中移除,但某些 JavaScript 仍引用该节点时,该节点被称为“分离”。已分离的 DOM 节点是导致内存泄漏的常见原因。本部分将介绍如何使用 DevTools 的堆性能分析器来识别分离的节点。
下面是一个简单的分离 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 节点之间分配的。
如需创建快照,请打开开发者工具并前往内存面板,选择堆快照单选按钮,然后按拍摄快照按钮。

快照可能需要一些时间来处理和加载。完成后,从左侧面板(名为堆快照)中选择该快照。
在类过滤条件输入框中输入 Detached,以搜索分离的 DOM 树。

展开脱离树即可进行调查。

点击节点可进一步调查。在 Objects 窗格中,您可以查看有关引用该对象的代码的更多信息。例如,在以下屏幕截图中,您可以看到 detachedTree 变量引用了该节点。如需修复此特定内存泄漏问题,您需要研究使用 detachedTree 的代码,并确保在不再需要该节点时移除对该节点的引用。

使用分配时间轴找出 JS 堆内存泄漏
分配时间轴是另一个可帮助您跟踪 JS 堆中的内存泄漏的工具。
为了演示分配时间轴,请考虑以下代码:
var x = [];
function grow() {
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
每次按下代码中引用的按钮时,都会向 x 数组添加一个包含 100 万个字符的字符串。
如需录制分配时间轴,请打开开发者工具,前往内存面板,选择时间轴上的分配单选按钮,按下 录制按钮,执行您怀疑会导致内存泄漏的操作,然后在完成后按下 停止录制按钮。
在录制过程中,请注意分配时间轴上是否显示了蓝色条,如以下屏幕截图所示。

这些蓝色条表示新的内存分配。这些新的内存分配就是可能发生内存泄漏的地方。您可以放大某个条形,以过滤 Constructor 窗格,使其仅显示在指定时间范围内分配的对象。

展开对象并点击其值,即可在对象窗格中查看有关该对象的更多详细信息。例如,在下面的屏幕截图中,通过查看新分配的对象的详细信息,您可以看到该对象已分配给 Window 作用域中的 x 变量。

按函数调查内存分配
在内存面板中使用分配抽样性能分析类型,按 JavaScript 函数查看内存分配情况。

- 选择分配采样单选按钮。如果页面上有工作器,您可以从选择 JavaScript 虚拟机实例窗口中选择该工作器作为性能分析目标。
- 按开始按钮。
- 在您要调查的页面上执行操作。
- 完成所有操作后,按停止按钮。
开发者工具会显示按函数划分的内存分配明细。默认视图为 Heavy (Bottom Up),该视图会在顶部显示分配内存最多的函数。

识别由 JS 引用保留的对象
已分离的元素配置文件会显示因被 JavaScript 代码引用而保留的已分离元素。
记录分离的元素配置文件,以查看确切的 HTML 节点和节点数。

发现频繁的垃圾回收
如果您的网页似乎经常暂停,则可能存在垃圾回收问题。
您可以使用 Chrome 任务管理器或时间轴内存记录来发现频繁的垃圾回收。在任务管理器中,频繁上升和下降的内存或 JavaScript 内存值表示频繁的垃圾回收。在时间轴记录中,JS 堆或节点计数图表频繁上升和下降表示频繁进行垃圾回收。
确定问题后,您可以使用分配时间轴记录来找出内存分配的位置以及导致分配的函数。