fuite: 一个在 web 应用中发现内存泄漏的工具

1,874 阅读6分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

介绍 fuite: 一个在 web 应用程序中发现内存泄漏的工具

在我们日常开发中,很少会去调试内存泄漏的问题,虽然是有工具可以实现调试的,但是它调试起来很复杂繁琐,通常也不会准确地告诉我们:我们的系统为什么会出现内存泄漏。

在我的印象中,JavaScript是一个自带垃圾回收机制的语言,应该很少会出现内存泄漏的问题。但是当我第一次开始研究内存泄漏时,却发现它却成为了内存泄漏的主要来源。随着研究的深入,我发现在单页面应用程序(Single Page Apps,简称 spa)上是普遍存在内存泄漏问题的,只是很少有人对这些应用做测试。

很少有web开发人员会注意到内存泄漏的问题,通常是因为他们发现自己的网站正在变慢,变卡甚至崩溃,或者偶然间在内存工具中发现页面占用了N兆(甚至N千兆)的内存时,才会考虑系统是否存在内存泄漏。不过当他们注意到这些问题的时候,系统出现内存泄露的点可能就不止一个了。

回顾我之前写过的关于内存泄露的文章就会发现,我通常都会建议开发中通过Chrome的内存监测工具来进行调试,但是由于工具的复杂程度,大部分人都很难通过它来查找除内存泄漏的原因。所以我希望有一个CLI工具来实现这些复杂的操作。

因此我写了一个工具fuite (法语“ leak”)。在这个CLI工具中,我们可以用通过它来分析任意的一个网址是否存在内存泄漏的问题。

npx fuite https://example.com

在执行命令时,fuite会默认网站是呈现给用户的一个SPA页面,之后会抓取页面中的内部链接(例如/about /contact)。获取到链接后,会按照下面步骤来执行:

  1. 点击链接
  2. 点击浏览器的返回按钮
  3. 重复操作来监测内存是否增长 当fuite监测到内存异常时,会告知我们哪些对象可能会导致内存泄漏。
Test         : Go to /foo and back
Memory change: +10 MB
Leak detected: Yes
Leaking objects:
| Object            | # added | Retained size increase |
| ----------------- | ------- | ---------------------- |
| HTMLIFrameElement | 1       | +10 MB                 |
Leaking event listeners:
| Event        | # added | Nodes  |
| ------------ | ------- | ------ |
| beforeunload | 2       | Window |
Leaking DOM nodes:
DOM size grew by 6 node(s) 

为了实现这个功能,我在fuite中使用了博客文章中阐述的基本策略。他会启动Chrome来运行n次(默认7次),并检查是否有对象发生内存泄漏了n次(7,14,21等)。

fuite同时会分析全部的 Array, Object, Map, Set,事件监听器以及整个DOM。假如一个数组在执行了7次后同时也增长了7次,那么这个对象就有可能发生了内存泄漏。

测试正式网站

在反复进行页面跳转并返回的操作时,会发现在许多spa应用中发现内存泄漏的问题。下面是我对10个前端主流框架的首页进行的测试结果,并发现他们都存在内存泄漏的问题。

站点是否发生泄漏内部连接数平均增长最大增长数
Site 1yes827.2 kB43 kB
Site 2yes1050.4 kB78.9 kB
Site 3yes2798.8 kB135 kB
Site 4yes8180 kB212 kB
Site 5yes13266 kB1.07 MB
Site 6yes8638 kB1.15 MB
Site 7yes71.37 MB2.25 MB
Site 8yes153.49 MB4.28 MB
Site 9yes435.57 MB7.37 MB
Site 10yes1614.9 MB186 MB

在这种情况下,内部链接是指所测试的内部链接数目,平均增长是指每个链接的平均内存增长(即每次跳转页面并返回) ,而最大增长是指内部链接内存泄漏的最大值。值得注意的时,这些数据并不是一次的测试结果而是fuite测试了7次之后的结果。

当我们要确定这些结果的真实性时,你可以使用Chrome DevTools的Memory工具。下面图片是我选择了测试中表现最差的站点,点击其中的一个链接,然后按下后退键。每次操作都保存一个堆得快照,然后重复操作。

74a805399e214e6a054c93ad6e9d4ba.jpg

我们可以发现每次当点击页面的时候,内存大概都会增加6M左右。

注意事项

我们要注意的是并不是所有的SPA中的内存泄漏都是需要解决的。比如,当SPA需要维护焦点和滚动状态,虽然会造成泄漏,但是却并不致命。fuite会提示我们这些小的问题,因为它确实导致了内存泄漏,但是作为开发者我们需要注意这种问题是否真的需要修改。

在开发过程中,一些内存的增长可能是由于浏览器内部的原因造成的(比如JITing),这是网页无法真正控制的。正因如此,内存增长并不意味着你的代码存在问题,很多情况下内存的增长是不可避免的。

在少数情况下,由于浏览器的一些bug也会造成内存的增长。在分析上面网站时,由于Chrome的bug导致站点#4出现了问题。由于fuite是程序,所以很难检测到是否是浏览器的问题。在fuite有警告的情况下,我们可以对比下其他浏览器,来确定问题究竟来源于何处。

说完了SPA的应用,在我们传统的MPA(多页面应用程序)中,就很少会出现内存泄漏的问题,因为每次当我们切换页面时,都会清除浏览器的内存。当然,这都是在浏览器没有bug的前提下。

Fuite目前主要就是我们当前浏览的网页的堆内存,所以当页面中含有iframe,web works,service works时,fuite暂时还是无法检测的。

其他内存泄漏的场景

fuite通过获取页面内部链接来进行测试只是一个默认的方案。Fuite是构建在 pupteer之上的,因此你可以通过编写自己的pupteer脚本来告知fuite如何去测试你想要测试的内容。提供一些可能需要测试的场景:

  1. 反复打开/关闭一个模态框
  2. 鼠标悬停在某一个元素上时,显示一些提示。当鼠标移出时,删除对应的DOM。
  3. 一个无限加载的列表,当我们离开该列表时

在这些场景中,可能会存在你意想不到的内存泄露的问题。当测试了许多应用后,就会发现很多对话框或者工具提示都存储着内存泄漏的问题。

归根结底,fuite只是一个基本工具,它并不可以100%的协助完成修复内存泄漏的工作,因为作为开发者你需要自己去判断是否需要修改或者保留泄漏的问题,在这之中寻找出一个合理的解决方案。但是我的目的是95%的工作都交给fuite来做。

相关链接

⛱ 周刊翻译计划预览地址

⛱ 周刊翻译计划GitHub

⛱ 原文地址

⛱ fuite GitHub

⛱ 作者视频