修复内存问题

优质
小牛编辑
129浏览
2023-12-01

了解如何使用Chrome DevTools查找影响网页性能的内存问题,包括内存泄漏,内存膨胀和频繁的垃圾回收。

TL;DR

  • 使用Chrome任务管理器,了解您的网页使用的内存量。
  • 使用时间轴记录可视化内存使用。
  • 使用堆快照标识分离的DOM树(内存泄漏的常见原因)。
  • 通过分配时间轴记录了解在JS堆中分配新内存的时间。

概述

在[RAIL][RAIL]性能模型中,你的重点应该是你的用户上。

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

  • 网页的性能效果会随着时间的推移逐渐变差。这可能是内存泄漏的症状。内存泄漏是指页面中的错误导致页面逐渐使用越来越多的内存。
  • 网页的效果始终不佳。这可能是内存膨胀的症状。内存膨胀是指当页面使用的内存比最佳页面速度所需的内存多出很多。
  • 网页的效果延迟或频繁暂停。这可能是频繁的垃圾回收的症状。垃圾回收是指浏览器回收内存。浏览器决定何时回收内存。在回收内存期间,所有脚本执行都将暂停。所以如果浏览器频繁的垃圾回收,,那脚本被暂停的次数也会很频繁。

内存膨胀︰ 多少才是“过多”?

内存泄漏很容易定义。如果一个网站逐渐使用越来越多的内存,那么说明你的网页有内存泄漏。但内存膨胀是有点难以定义。我们用什么来定义“使用太多的内存”呢?

这个没有硬性的准则,因为不同的设备和浏览器具有不同的性能表现。在高端智能手机上平滑运行的同一页面在低端智能手机上可能崩溃。

这里的关键是使用RAIL模型,并专注于您的用户。了解你用户主要使用哪些设备,然后在这些设备上测试您的网页。如果表现一直不好,该页面所需内存可能超出那些设备的内存存储能力。

使用Chrome任务管理器实时监控内存使用情况

使用Chrome任务管理器作为检测内存问题的起点。任务管理器是一个实时监视器,它告诉你一个页面当前正在使用多少内存。

  • 按快捷键Shift+Esc或 打开Chrome main menu(主选单),然后选择More tools(更多工具) > Task manager(任务管理器),可以开启任务管理器。


  • 右键点击Task manager(任务管理器)表头栏,并且启用JavaScript memory(JavaScript 使用的内存)。

这两列是在用不同角度来告诉你,网页的内存使用情况︰

Memory(内存)列 表示本机内存。DOM节点存储在本机内存中。如果这个值在增加,则说明正在创建DOM节点。

JavaScript Memory(JavaScript 使用的内存)列 表示JS堆。这一列包含两个值。 您感兴趣的值是会跳动的数字(括号中的数字)。跳动的数字表示您网页上的可获得的对象正在使用多少内存。如果这个数字在增加,那说明正在创建新对象,或现有对象正在增长。

什么是可获得的对象?

浏览器的垃圾回收机制采用标记-清除垃圾回收算法。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期的从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……有引用的对象可获得的对象(reachable objects),也叫活动对象(Live Object)。没有引用的对象是不可获得的对象(non-reachable objects),被认为是垃圾,可以被回收。

从根开始,垃圾回收器将找到所有可以获得的对象和所有不可获得的对象。

使用Timeline(时间轴)录制可视化内存泄漏

您还可以使用Timeline(时间轴)面板来查看内存使用情况。Timeline(时间轴)面板可为你更直观地实时显示页面的内存使用。

  • 打开DevTools上的Timeline(时间轴)面板。
  • 启用Memory(内存)复选框。
  • 进行录音。

提示:使用强制垃圾回收开始和结束你的录制是一个好习惯。在录制时,点击collect garbage(垃圾回收)按钮(强制垃圾回收按钮)可以强制回收垃圾。

为了演示时间轴内存录制,考虑下面的代码:

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);

每次点击代码中引用的按钮(ID为grow)时,1万个div节点被附加到文档<body>中,并且一个100万个x字符的字符串被pushx数组中。运行此代码将生成一个Timeline(时间轴)录制,如以下截图所示:

首先,介绍一下界面。Overview(概述)窗格(NET下面)中的HEAP(堆)曲线图表示JS堆。 在Overview(概述)下方是Counter(计数器)窗格。在这里你可以看到内存使用情况(与Overview(概述)窗格中的HEAP(堆)曲线图相同),分别显示以下内容:JS heap(JS堆),documents(文档),DOM nodes(DOM节点),listeners(侦听器)和GPU memory(GPU内存)。勾选或取消勾选复选框可以将其从图表中显示或隐藏。

现在,分析代码与截图进行比较。你可以看到节点计数器(绿色曲线),你可以看到它与代码完全匹配。节点计数很规律的逐步增加。您可以认为节点计数的每次增加都是对grow()的调用。JS堆曲线图(蓝色曲线)就不是那么直截了当了。最佳实践是,第一次下降实际上是一个强制垃圾回收(通过点击collect garbage(垃圾回收)按钮实现的)。随着记录的进行,你可以看到JS堆大小峰值。这是很自然,而且符合预期的:在每次按钮(ID为grow)点击时,JavaScript代码创建DOM节点,并在创建100万个字符的字符串时需要做大量工作。最关键的一点是,JS 堆结束时比起始点 (这里“起始点”是指强制垃圾回收之后的点) 高。在真实情况下,如果你看到JS堆曲线或节点曲线逐渐增加,它可能意味着内存泄漏。

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

当一个DOM节点没有来自页面的DOM树或JavaScript代码的引用,他们就会被当做垃圾回收。当一个节点从DOM树中移除时,它被称为detached(分离DOM树的节点),但是一些JavaScript仍然引用它。分离的DOM节点是内存泄漏的常见原因。本节教你如何使用DevTools的堆分析器来识别分离的节点。

这里有一个简单的分离DOM节点的例子。

var detachedNodes;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);

单击代码中引用的按钮(ID为create)将创建一个具有十个li子节点的ul节点。这些节点由 JavaScript 代码引用,但不存在于DOM树中,因此它们是分离节点。

堆快照是识别分离节点的一种方法。顾名思义,堆快照,在该快照的时间点上,显示内存是如何分布在页面的JS对象和DOM节点之间。

要想创建快照,打开DevTools,并转到Profiles(分析)面板,勾选Take Heap Snapshot(采集堆快照)单选按钮,然后点击Take Snapshot(采集快照)按钮。

4.png

快照可能需要一些时间来处理和加载。一旦完成,在面板的左侧(名为HEAP SNAPSHOTS(堆快照))的标签下,选中它。

Class filter(类别过滤器)文本框中输入Detached可以搜索分离的DOM树。

5.png

点击三角图标,可以展开分离树,查看研究详情。

6.png

黄色高亮的节点表示是通过JavaScript代码直接引用它们。红色高亮的节点没有直接引用。它们依然存在与内存中,因为它们是黄节点树的一部分。一般来说,你应该把重点放在黄色的节点上。修复您的代码,以便黄色节点不活动的时间比它活动的时间长,并且你也可以去除作为黄色节点树一部分的红色节点。

点击一个黄色的节点进一步研究。在Objects(对象)窗格中,您可以查看有关引用它的代码的更多信息。例如,在下面的屏幕截图中,您可以看到detachedTree变量引用了该节点。为了解决这个详细说明的内存泄漏,您将研究使用detachedTree的代码,并确保,当不再需要它的时候,删除它对节点的引用。

7.png

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

Allocation Timeline(分配时间轴)是帮助您跟踪JS堆中的内存泄漏的另一个工具。

要演示分配时间线,请考虑以下代码:

var x = [];function grow() {
  x.push(new Array(1000000).join('x'));
}document.getElementById('grow').addEventListener('click', grow);

每次点击代码中引用的按钮(ID为'grow')时,将向x数组中添加100万长度的字符串。

要想录制分配时间线,打开DevTools,转到Profiles(分析)面板, 选择Record Allocation Timeline(录制分配时间轴)单选按钮, 点击Start(开始)按钮,执行您怀疑导致内存泄漏的操作, 然后,当你完成时,点击stop recording(停止录制)按钮(停止录制按钮)。

在您录制时,请注意是否有蓝色条显示在Allocation Timeline(分配时间轴)上,就像下面的截图。

8.png

那些蓝色条表示新的内存分配。这些新的内存分配可能就是内存泄漏点。您可以缩放条形来筛选,以便在Constructor(构造函数)”窗格只显示在指定时间范围内分配的对象。

9.png

展开对象并单击其值,可以在Object(对象)窗格中查看其详细信息。 例如,在下面的屏幕截图中,通过查看新分配的对象的详细信息, 您将能够看到它已分配给Window作用域中的x变量。

在Object面板中,点击想要查看的对象,展开以查看更多详细信息。例如,下面的屏幕截图,通过查看新分配对象的详细信息,你将能够看到x变量在Window范围内。

10.png

按函数查看内存分配

使用Record Allocation Profiler(录制分配分析器)类型,可以按JavaScript函数查看内存分配。

11.png

  1. 勾选Record Allocation Profiler(录制分配分析器)单选按钮。如果页面上有一个worker(这里指Service workers),您可以使用Start(开始)按钮旁边的下拉菜单选择它作为性能分析目标。
  2. Start(开始)按钮。
  3. 在要查看的页面上执行操作。
  4. 当你完成所有操作后,按Stop(停止)按钮。

DevTools按函数显示了内存分配的明细。默认视图是Heavy (Bottom Up)(内存分配从高到底排序),显示在最顶部的是分配内存最多的函数。

12.png

跟踪频繁的垃圾回收

如果您的网页频繁出现卡顿现象,那么你的网页可能有垃圾回收的问题。

您可以使用Chrome Task Manager(Chrome任务管理器)或时间轴内存记录来发现频繁的垃圾回收。 在Task Manager(任务管理器)中,MemoryJavaScript Memory值频繁地上升和下降代表垃圾回收频繁。 在时间轴录制中,JS堆或节点计数图频繁地上升和下降表明垃圾回收频繁。

一旦你确定了问题,您可以使用Allocation Timeline(分配时间轴)录制来查找分配内存的位置和导致分配的函数。