大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

一般来说,内存泄漏是指软件保留了其不再需要的一块内存的情况,比如:JavaScript 中对不需要对象的无意引用。而对于垃圾收集器来说,无法区分仍在使用对象和无意引用对象。
Web 开发人员通常无需关注内存泄漏。 这是因为,页面上每个链接都会导致加载新页面从而清除内存。 而且内存泄漏通常非常隐秘,只有当特定程序长时间运行时才会变得明显。

但是,随着单页应用程序 SPA 和渐进式 Web 应用程序 PWA 的出现,情况已经悄然变化。 许多网站的行为确实像应用程序一样可以长时间运行,对于使用 Web Audio API 的应用程序来说尤其如此。
最简单的内存泄漏示例是将一些元数据附加到对象。 假设有几个对象,并且想为每个对象存储一些元数据。 但又不想在对象上设置属性,即希望将元数据保存在单独的位置。
此时,可以通过使用 Map 来解决,如以下代码片段所示。 其允许存储一些元数据,将其恢复并再次删除,所需要的只是一个使用对象作为索引其元数据的键的 Map。
const map = new Map();// store metadatamap.set(obj, metadata);// get metadatamap.get(obj);// delete metadatamap.delete(obj);但是,如果带有元数据的对象不再在其他地方被引用怎么办? 即仍然无法被垃圾回收,因为 Map 仍然持有对对象的引用来索引元数据。
const map = new Map();setInterval(() => { const obj = { }; map.set(obj, { any: 'metadata'});}, 100);以前我写过一篇关于 queryObjects 的文章,queryObjects 的黑魔法是会在每次调用之前触发一次垃圾回收,接下来看看以上代码输出情况:
const map = new Map();const nativeQuery = queryObjects;// 保存引用setInterval(() => { const obj = { }; map.set(obj, { any: 'metadata'}); nativeQuery(Map) // 可以看到 map 一直不会回收(对象越来越多),内存持续增加}, 100);总之,所有创建的对象都会在每次垃圾回收中幸存下来,因为 Map 仍然持有对它们的引用。此时可以通过 WeakMap 解决,因为 WeakMap 持有的引用不会阻止任何对象被垃圾收集。
const map = new WeakMap();const nativeQuery = queryObjects;setInterval(() => { const obj = { }; map.set(obj, { any: 'metadata'}); nativeQuery(WeakMap)}, 100);即,通过用 WeakMap 替换 Map,可以消除当前示例的内存泄漏。
2.用 Puppeteer 自动化排查内存泄漏Puppeteer 是一个可用于远程控制 Chrome 或任何其他 Chromium 浏览器的工具,是 Selenium 和 WebDriver 的更简单的替代品,但缺点是它目前仅适用于基于 Chromium 的浏览器。 Puppeteer 可以访问 Selenium 无法访问的一些 API,因为其尝试像真实用户一样与网站进行交互。
另一方面,Puppeteer 可以访问许多普通用户无法访问的 API,本质是通过利用 Chrome DevTools 协议来实现的。比如: Puppeteer 可以做但 Selenium 不能做的事情之一就是检查内存,而当尝试查找内存泄漏时则非常有用。
Puppeteer 的 API 提供了跟踪内存使用情况所需的所有功能,即 page.metrics() 方法,其返回一个名为 JSHeapUsedSize 的指标,代表 Chrome 中使用的 JavaScript 引擎 V8 用作内存的字节数。
const {JSHeapUsedSize} = await page.metrics();触发垃圾收集JavaScript 程序的内存由圾收集器管理。与现实世界中的垃圾收集不同,JavaScript 垃圾收集器通常会按照非常严格的时间表进行,只要其认为是正确的时间,就会执行其工作,但通常不能从 JavaScript 代码中触发。
但有必要在检查内存之前运行,以确保所有垃圾都已被收集,并且内存消耗已根据代码所做的最新更改计算出来。
Puppeteer 提供了一种更简单、更好的 page.queryObjects() 方法来一次性完成查询具有给定原型的所有对象。
如以下函数所示,其采用原型 prototype 并计算原型链中具有相同原型的所有对象。 在本例中,使用了 Object.prototype,因为在 JavaScript 中几乎所有内容都继承它,除了没有原型的对象,比如:通过使用 Object.create(null) 创建的对象和 JavaScript 原始值。
const countObjects = async (page) => { const prototype = await page.evaluateHandle(() => { return Object.prototype; }); const objects = await page.queryObjects( prototype ); // 查询给定原型所有对象 const numberOfObjects = await page.evaluate((instances) => instances.length, objects ); await prototype.dispose(); await objects.dispose(); // 释放句柄引用的对象以进行垃圾回收 return numberOfObjects;};以上代码有几点需要说明:
page.evaluateHandle :和 page.evaluate 之间的唯一区别是 evaluateHandle 将返回包装在 page 对象中的值。同时,可以传递字符串而不是函数(尽管建议使用函数,因为它们更容易调试并与 TypeScript 一起使用)page.queryObjects:迭代 JavaScript 堆并查找具有给定原型的所有对象page.evaluate:执行页面上下文中的函数并返回结果该技术与对象在堆上占用的字节数无关,只是计算它们的数量且至少是测试 JavaScript 代码中的内存泄漏的一个良好的开始。
这是一种获得一致结果的方法,最好的一点是,其还会在对对象进行计数之前在内部触发垃圾收集。
运行内存泄漏测试以下示例使用 Mocha 来运行测试,但其他测试库的设置应该非常相似。 与许多其他测试库一样,Mocha 提供了在测试之前和之后触发的钩子。 开发者可用于通过 Puppeteer 启动浏览器,并在测试完成后再次关闭。
describe('memory leak tests', () => { let browser; let context; let page; before(async () => { browser = await puppeteer.launch(); }); beforeEach(async () => { context = await browser .createIncognitoBrowserContext(); page = await context.newPage(); }); it('should ...', () => { // The actual test will be executed here. }); afterEach(() => context.close()); after(() => browser.close());});上面的代码在第一次测试之前启动浏览器,并在最后一次测试后关闭。 此外,它在每次测试之前都会创建一个带有页面的新上下文, 每次测试后还会关闭该上下文。
现在可以使用 countObjects() 函数来编写实际的内存泄漏测试,如下所示:
it('should not have a memory leak', async () => { const numberOfObjects = await countObjects(page); await page.evaluate(() => { // Do something a couple of times. }); expect(await countObjects(page)) .to.equal(numberOfObjects);});至此,终于有了一个以自动化方式测试内存泄漏的解决方案。
如果有兴趣,可以继续阅读为 standardized-audio-context 库编写的内存泄漏测试,以确保报告的内存泄漏永远不会回来。
参考资料https://media-codings.com/articles/automatically-detect-memory-leaks-with-puppeteer
https://javascript.plainenglish.io/these-5-bad-javascript-practices-will-lead-to-memory-leaks-and-break-your-program-9cf692303043
https://dev.to/sdkdeepa/puppeteer-web-automation-198
https://nolanlawson.com/2022/01/05/memory-leaks-the-forgotten-side-of-web-performance/
https://holinka.dev/blog/how-puppeteer-and-a-b-testing-helped-in-refactoring/