应对Web应用程序中的内存泄漏

当我们从在服务器上形成页面的网站的开发转移到在客户端上呈现的单页Web应用程序的创建时,我们采用了某些游戏规则。其中之一是准确处理用户设备上的资源。这意味着-不要阻塞主流,不要“旋转”笔记本电脑的风扇,不要放手机的电池。我们交换了Web项目交互性方面的一个改进,即它们的行为变得更像普通应用程序的行为,这是服务器呈现世界中不存在的一类新问题。



这样的问题之一就是内存泄漏。设计不佳的一页应用程序可能会轻易吞噬兆字节甚至千兆字节的内存。即使它安静地位于背景选项卡上,它也可以占用越来越多的资源。在捕获了大量资源后,此类应用程序的页面可能开始大大“变慢”。另外,浏览器可以简单地关闭选项卡并告诉用户:“出了点问题。”


出问题了

当然,在服务器上呈现的站点也可能遭受内存泄漏问题。但是这里我们谈论的是服务器内存。同时,由于每个用户在页面之间切换之后浏览器都会清除内存,因此此类应用程序不太可能导致客户端发生内存泄漏。

Web开发出版物中没有很好地讨论内存泄漏的主题。而且尽管如此,我几乎可以确定大多数非平凡的单页应用程序都会遭受内存泄漏的影响-除非处理它们的团队拥有可靠的工具来检测和修复此问题。这里的要点是,在JavaScript中,随机分配一定数量的内存,然后忘记释放该内存非常容易。

这篇文章的作者(我们今天将发表其翻译版本)将与读者分享他在解决Web应用程序中的内存泄漏方面的经验,并希望举例说明其有效检测。

为什么这么少写?


首先,我想谈谈为什么很少写关于内存泄漏的文章。我想在这里您可以找到几个原因:

  • 缺乏用户抱怨:大多数用户在浏览Web时并不忙于密切监视任务管理器。通常,在内存泄漏严重到导致无法工作或降低应用程序速度之前,开发人员不会遇到用户的抱怨。
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


用于开发Web应用程序的现代库和框架(例如React,Vue和Svelte)使用应用程序的组件模型。在此模型中,导致内存泄漏的最常见方法是这样的:

window.addEventListener('message', this.onMessage.bind(this));

就这样。这就是为项目“配备”内存泄漏所需的全部工具。为此,只需调用某些全局对象(例如,或或类似的东西)addEventListener方法,然后在卸载组件时,忘记使用removeEventListener方法删除事件侦听 但是,这样做的后果甚至更糟,因为会发生整个组件的泄漏。这是由于该方法已附加到的事实与此组件一起,发生其子组件的泄漏。与该组件关联的所有DOM节点很可能会泄漏。结果,这种情况可能会很快失控,从而导致非常严重的后果。window<body>

this.onMessagethis

解决此问题的方法如下:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

内存泄漏最常发生的情况


经验告诉我,使用以下API时,内存泄漏最常发生:

  1. 方法addEventListener这是最经常发生内存泄漏的地方。要解决该问题,在正确的时间打电话就足够了removeEventListener
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. 由全局对象表示的存储库。当您使用Redux之类的东西来控制应用程序的状态时,状态存储由全局对象表示。结果,如果您不小心处理此类存储,则不会从其中删除不必要的数据,结果,其大小将不断增加。
  6. 无限的DOM增长。如果该页面在不使用虚拟化的情况下实现了无限滚动,则意味着该页面上的DOM节点数可以无限增加。

上面,我们检查了内存泄漏最常发生的情况,但是,当然,还有许多其他情况导致我们感兴趣的问题。

内存泄漏识别


现在,我们进入了识别内存泄漏的挑战。首先,我认为任何现有工具都不适合于此。我尝试了Firefox内存分析工具,还尝试了Edge和IE中的工具。测试了Windows Performance Analyzer。但是,这些工具中最好的还是Chrome开发者工具。的确,在这些工具中有许多“尖角”,这是值得了解的。

在Chrome开发人员提供的工具中,我们最感兴趣Heap snapshot的选项卡是Profiler Memory,它可让您创建堆快照。还有其他用于在Chrome中分析内存的工具,但我无法从中提取出检测内存泄漏的特殊好处。


堆快照工具可让您对主流,Web worker或iframe元素的内存进行快照。

如果Chrome工具窗口类似于上图所示,则当您单击按钮时Take snapshot,将捕获有关所选虚拟机内存中所有对象的信息。被调查页面的JavaScript。这包括在中window引用的对象,在调用中使用的回调引用的对象setInterval,等等。内存快照可被视为被调查实体工作的“冻结时刻”,代表有关该实体使用的所有内存的信息。

拍照后,我们进入寻找泄漏的下一步。它在于重现一种场景,在这种场景下,根据开发人员的说法,可能发生内存泄漏。例如,它正在打开和关闭某个模式窗口。关闭类似的窗口后,预期分配的内存量将返回到打开窗口之前的水平。因此,他们会拍摄另一张照片,然后将其与之前拍摄的照片进行比较。实际上,图像比较是我们感兴趣的最重要的特征Heap snapshot


我们拍摄第一个快照,然后执行可能导致内存泄漏的操作,然后拍摄另一个快照。如果没有泄漏,分配的内存大小将是相等的,

的确,这Heap snapshot远非理想的工具。它有一些局限性值得了解:

  1. 即使单击Memory开始垃圾收集的面板上的小按钮Collect garbage),也要确保确实清除了内存,您可能需要连续拍摄几张照片。我通常有三枪。这里值得关注每个图像的总大小-最后应该稳定下来。
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

此时,如果您的应用程序非常复杂,则在比较快照时您可能会注意到很多“泄漏”对象。这里的情况有些复杂,因为并非总是会被误认为是内存泄漏。可疑的大部分只是用于处理对象的正常过程。清除某些对象占用的内存,以将其他对象放置在此内存中,将某些内容刷新到缓存中,以便不会立即清除相应的内存,依此类推。

我们克服信息噪音


我发现突破信息噪声的最好方法是重复那些可能导致内存泄漏的操作。例如,代替在捕获第一张照片后仅打开和关闭模态窗口一次,这可以完成7次。为什么是7?是的,只是因为7是一个引人注目的素数。然后,您需要拍摄第二张照片,并将其与第一张照片进行比较,以确定某个物体是否“泄漏”了7次(或14次或21次)。


比较堆快照。请注意,我们正在比较3号图和6号图。事实是,我连续拍摄了三张照片,这样Chrome会有更多的垃圾回收会话。此外,请注意,有些物体“泄漏”了7次,

另一个有用的窍门是,在研究开始时,在创建第一张图片之前,请执行一次该过程,然后按预期进行:内存泄漏。如果在项目中使用代码拆分,则特别推荐这样做。在这种情况下,很可能在首次执行可疑操作时,将加载必要的JavaScript模块,这将影响分配的内存量。

现在,您可能有一个问题,为什么您应该特别注意对象的数量,而不是总的内存量。在这里我们可以说我们在直觉上努力减少“泄漏”内存的数量。在这方面,您可能认为应该监视所使用的内存总量。但是,由于一个重要原因,这种方法不太适合我们。

如果某事“泄漏”,是因为(拒绝Joe Armstrong)您需要一根香蕉,但最终得到的是香蕉,容纳它的大猩猩,以及整个丛林。如果我们专注于内存总量,那将与“测量”丛林相同,而不是让我们感兴趣的香蕉。


大猩猩在吃香蕉,

现在回到上面的例子addEventListener。泄漏源是引用函数的事件侦听器。而此功能又是指可能会存储指向一堆好东西(例如数组,字符串和对象)的链接的组件。

如果您分析图像之间的差异,并按照实体所占用的内存量对其进行排序,则可以查看许多阵列,线,对象,其中大多数与泄漏无关。毕竟,我们需要找到所有事件的监听器。与他所指的相比,他几乎没有记忆。为了修复泄漏,您需要找到一根香蕉,而不是丛林。

结果,如果按“泄漏”对象的数量对记录进行排序,则会发现7个事件侦听器。也许有7个组件和14个子组件,也许还有其他类似的东西。这个数字7应该从全局中脱颖而出,因为它仍然是一个相当引人注目的且不寻常的数字。在这种情况下,重复可疑动作多少次并不重要。检查图像时,如果怀疑是合理的,则将记录尽可能多的“泄漏”对象。这样可以快速确定内存泄漏的来源。

链接树分析


用于创建快照的工具提供了查看“链接链”的功能,这些链接可帮助您找出其他对象引用了哪些对象。这就是允许应用程序运行的原因。通过分析链接的“链”或“树”,您可以准确地找到为“泄漏”对象分配的内存。


链接链使您可以找出哪个对象引用了“泄漏”对象。在阅读这些链时,有必要考虑位于它们下面的对象是指位于上面的对象,

在上面的示例中,事件侦听器引用someObject的闭包(context)中有一个称为referenced的变量如果单击指向源代码的链接,将显示该程序相当容易理解的文本:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

如果我们将此代码与上一个图进行比较,结果发现context该图是onMessage引用的闭包someObject这是一个人为的例子实际内存泄漏可能不那么明显。

值得注意的是,堆快照工具有一些限制:

  1. 如果保存快照文件然后再次上传,则带有代码的文件的链接将丢失。也就是说,例如,下载了快照后,将不可能发现事件侦听器关闭代码在文件的第22行上foo.js由于此信息非常重要,因此保存堆快照文件或将其传输给某人几乎是无用的。
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

这是对我识别内存泄漏的基本策略的描述。我已经成功地使用了这种技术来检测许多泄漏。

没错,我必须说,这份查找内存泄漏的指南仅涵盖了现实中的一小部分。这仅仅是工作的开始。此外,您需要能够处理断点的安装,记录日志,测试更正,以确定它们是否解决了问题。而且,不幸的是,从本质上讲,所有这些都转化为对时间的认真投入。

自动内存泄漏分析


我想以无法自动检测内存泄漏的好方法开始这一部分。 Chrome拥有自己的performance.memory API ,但出于隐私原因,它不允许您收集足够详细的数据。结果,该API不能用于生产中以检测泄漏。 W3C Web性能工作组先前讨论了内存工具,但其成员尚未就旨在替代该API的新标准达成共识。

在测试环境中,您可以performance.memory使用Chrome标志--enable-precise-memory-info来增加数据输出的粒度。仍然可以使用Chromedriver自己的团队创建堆快照:takeHeapSnapshot。该团队具有我们已经讨论过的相同限制。如果您使用此命令,则由于上述原因,有可能要对其进行三次调用,然后仅取回最后一次调用所收到的内容,这很有可能。

由于事件侦听器是最常见的内存泄漏源,因此我将介绍我使用的另一种泄漏检测技术。它包括在创造的API猴子补丁addEventListener,并removeEventListener在计数的链接,以检查他们的数量归零。这是如何完成此操作的示例。

在Chrome开发人员工具中,您还可以使用getEventListeners本机API 找出将哪些事件侦听器附加到特定元素。但是,此命令仅在开发人员工具栏中可用。

我想补充一点,Matthias Binens告诉我了另一个有用的Chrome工具API。这些是queryObjects使用它,您可以获得有关使用特定构造函数创建的所有对象的信息。这是有关在Puppeteer中自动进行内存泄漏检测的一些很好的材料。

摘要


在Web应用程序中搜索和修复内存泄漏仍处于起步阶段。在这里,我谈到了一些性能良好的技术。但是应该认识到,这些技术的应用仍然充满某些困难和耗时。

正如他们所说的,与任何性能问题一样,提前捏值得一磅。也许有人会发现准备适当的综合测试而不是在泄漏发生后进行分析是有用的。如果不是一个泄漏而是多个泄漏,那么对问题的分析可能会变成洋葱剥皮:解决一个问题后,发现另一个问题,然后重复此过程(并且一直如此,就像洋葱一样) ,眼泪)。代码审查还可以帮助确定常见的泄漏模式。但这(如果您知道)在哪里看。

JavaScript是一种可以安全使用内存的语言。因此,具有讽刺意味的是,Web应用程序中发生内存泄漏的难易程度。没错,这部分是由于设备用户界面的功能。您需要听很多事件:鼠标事件,滚动事件,键盘事件。应用所有这些模式很容易导致内存泄漏。但是,努力确保我们的Web应用程序可以节余地使用内存,我们可以提高其性能并保护它们免受“崩溃”的影响。另外,我们由此证明了对用户设备资源限制的尊重。

亲爱的读者们!您是否在Web项目中遇到内存泄漏?


All Articles