Memlab:用于检测浏览器和 Node.js 内存泄漏的 JavaScript 堆分析工具

Memlab 是 Meta 推出的端到端测试与分析框架,专门用于识别 JavaScript 内存泄漏并发现优化机会。
该工具提供完整的工作流程来自动化内存泄漏检测:模拟用户与单页应用的交互、获取 JavaScript 堆快照、对快照进行深度分析、过滤无关数据、聚合相似的泄漏模式,并生成详细的保留器追踪路径供调试使用。
安装配置
通过 npm 全局安装 Memlab:
npm install -g memlab
memlab help
示例应用:检测分离 DOM 元素
本教程演示如何利用 Memlab 检测被分离的 DOM 元素导致的内存泄漏。示例应用源代码位于官方仓库:
准备演示应用
点击"Create detached DOMs"按钮会创建 1024 个分离的 DOM 元素,这些元素被 window 对象引用从而无法被垃圾回收。

// @nolint
import Link from 'next/link';
import React from 'react';
export default function DetachedDom() {
const handleCreateElements = () => {
if (!window.cachedElements) {
window.cachedElements = [];
}
for (let i = 0; i < 1024; i++) {
const element = document.createElement('div');
window.cachedElements.push(element);
}
console.log('分离的 DOM 元素已创建,请查看开发者工具 Memory 面板');
};
return (
<div className="container">
<div className="row">
<Link href="/">返回</Link>
</div>
<br />
<div className="row">
<button type="button" className="btn" onClick={handleCreateElements}>
创建分离 DOM 元素
</button>
</div>
</div>
);
}
源文件:packages/e2e/static/example/pages/examples/detached-dom.jsx
步骤一:克隆项目仓库
将 memlab 仓库克隆到本地:
git clone git@github.com:facebookincubator/memlab.git
步骤二:启动示例应用
在项目根目录执行以下命令启动 Next.js 示例应用:
cd packages/e2e/static/example
npm install
npm run dev
确保应用已在 http://localhost:3000 运行,然后选择 "Example 1" 进行测试。

执行内存泄漏检测
创建场景定义文件
// @nolint
// memlab/packages/e2e/static/example/scenario/detached-dom.js
/**
* 定义测试场景的入口 URL。
*/
function url() {
return "http://localhost:3000/examples/detached-dom";
}
/**
* 定义执行测试操作的交互逻辑。
* 此操作用于验证是否存在内存泄漏。
*
* @param page - Puppeteer 页面对象:
* https://pptr.dev/api/puppeteer.page/
*/
async function action(page) {
const targetButton = await page.$x(
"//button[contains(., '创建分离 DOM 元素')]"
);
const [button] = targetButton;
if (button) {
await button.click();
}
// 清理外部引用资源
await Promise.all(targetButton.map(e => e.dispose()));
}
/**
* 定义还原操作的交互逻辑。
* 用于恢复执行 action 前的页面状态。
*
* @param page - Puppeteer 页面对象:
* https://pptr.dev/api/puppeteer.page/
*/
async function back(page) {
await page.click('a[href="/"]');
}
module.exports = { action, back, url };
场景文件保存路径:packages/e2e/static/example/scenario/detached-dom.js
执行 Memlab 分析
运行以下命令开始分析(首次运行可能需要几分钟):
cd packages/e2e/static/example
npm run dev # 确保示例应用正在运行
memlab run --scenario scenarios/detached-dom.js

分析泄漏追踪结果
Memlab 会为每组泄漏对象输出具有代表性的追踪信息。

第一部分:浏览器交互记录
展示按照场景配置执行的浏览器操作序列:
page-load[6.5MB](baseline)[s1]- 初始页面加载时的 JavaScript 堆大小,baseline 堆快照保存为 s1.heapsnapshotaction-on-page[6.6MB](baseline)[s2]- 点击"创建分离 DOM 元素"按钮后的堆大小revert[7MB](final)[s3]- 离开触发泄漏的页面后的最终堆大小
第二部分:泄漏摘要统计
1024 leaks- 共检测到 1024 个泄漏对象,示例应用在 for 循环中创建了 1024 个分离 DOM 元素Retained size- 泄漏对象集群的聚合保留大小为 143.3KB(内存泄漏按保留跟踪相似性分组)
第三部分:详细泄漏追踪
泄漏追踪是从 GC 根(垃圾回收器遍历堆图时的入口对象)到泄漏对象的对象引用链。追踪揭示泄漏对象为何及如何保持在内存中。打破引用链可使泄漏对象被垃圾回收。
通过从原生 Window 对象(GC 根)向下追踪泄漏路径,可以定位到需要置空的引用。
map- 对象的 V8 HiddenClass(V8 内部用于存储对象形状元信息和原型引用,可忽略此实现细节)- https://v8.dev/blog/fast-properties#hiddenclasses-and-descriptorarrays
prototype- Window 类的实例cachedElements- Window 对象的属性,保留大小 148.5KB,指向 Array 对象0- 分离的 HTMLDIVElement(未连接到 DOM 树的 DOM 元素)存储在数组的第一个位置(由于打印全部 1024 条追踪信息过于冗长,Memlab 仅输出代表性追踪,即属性 0 而非 0->1023)
泄漏路径如下:
[window](object) -> cachedElements(property) -> [Array](object)
-> 0(element) -> [Detached HTMLDIVElement](native)
与示例代码中的泄漏逻辑对应:
window.cachedElements = [];
for (let i = 0; i < 1024; i++) {
window.cachedElements.push(document.createElement('div'));
}