- 开发工具
- 测试
- CLI
- MCP 服务
【免费下载链接】memlab
A framework for finding JavaScript memory leaks and analyzing heap snapshots
本篇围绕 memlab 内置堆分析插件CollectionsHoldingStaleAnalysis(CLI 命令名collections-with-stale)展开:它用于在单个堆快照中找出仍持有“陈旧对象”(detached DOM 节点、未卸载的 React Fiber 节点)的Map、Set、Array集合。读完本文,你可以直接用memlab analyze collections-with-stale或@memlab/heap-analysis的编程 API 定位“集合把已卸载/已移除对象一直拽在堆里”这类典型泄漏,并理解其底层扫描逻辑、stale 判定规则与输出格式。
什么是“持有陈旧对象的集合”
前端应用中很常见的泄漏模式是:组件卸载或 DOM 节点被移除后,本应可被垃圾回收的对象仍被某个长生命周期的集合(缓存、监听器注册表、索引表等)强引用着。这类集合里的元素可以被称为“陈旧对象”(stale objects)。在堆快照的视角下,这些陈旧对象有两类典型特征:
- Detached DOM 节点:已从文档树移除、但仍未被回收的 DOM 元素(例如
Detached HTMLDivElement); - 未卸载的 Fiber 节点(Detached FiberNode):React 组件卸载后残留在堆中的 Fiber 对象。
CollectionsHoldingStaleAnalysis就是专门针对这两种情况做的单快照分析:它扫描堆中的Map、Set、Array,统计每个集合里有多少直接子元素处于 detached 状态,并按陈旧元素占用的 retained size 排序输出,帮助开发者快速锁定“哪个集合、多大、里面有哪些陈旧元素”。
类结构与继承关系
根据官方 API 文档 CollectionsHoldingStaleAnalysis,该类的定义与成员如下:
| 成员 | 说明 |
|---|---|
| 构造函数 | new CollectionsHoldingStaleAnalysis(),无参数 |
| 继承自 | BaseAnalysis(进而继承内部Analysis抽象类) |
getCommandName() | 覆写基类方法,返回 CLI 命令名collections-with-stale,用于memlab analyze <ANALYSIS_NAME>引用 |
analyzeSnapshotFromFile(file, options?) | 继承自基类,对单个.heapsnapshot文件运行分析,返回Promise<AnalyzeSnapshotResult> |
源码位于 CollectionsHoldingStaleAnalysis.ts,并经由 heap-analysis 包入口 导出:
// packages/heap-analysis/src/plugins/CollectionsHoldingStaleAnalysis.ts export default class CollectionsHoldingStaleAnalysis extends BaseAnalysis { public getCommandName(): string { return 'collections-with-stale'; } /** @internal */ public getDescription(): string { return 'Analyze collections holding stale objects'; } // ... }getCommandName()的返回值同时决定了 CLI 侧的调用方式,getDescription()则会显示在memlab analyze list的输出中。
CLI 使用方式
在 memlab 命令行工具中,该分析对应memlab analyze的collections-with-stale子命令(见 CLI-commands.md):
memlab analyze collections-with-stale它接受的插件级参数有两个(源码中由getOptions()返回SnapshotFileOption与OutputOption):
| 参数 | 说明 |
|---|---|
--snapshot | 指定待分析堆快照文件的路径(.heapsnapshot) |
--output | 分析结果输出格式,可选text/json,默认text |
此外还支持memlab analyze的全局选项,例如--work-dir设置本次分析的工作目录、--verbose显示更多细节、--silent静音终端输出等。一个典型用法是:
# 对指定快照运行集合陈旧对象分析,输出 JSON 便于脚本处理 memlab analyze collections-with-stale \ --snapshot=./memlab-logs/tab-0/2026-10-12-14-00-00.heapsnapshot \ --output=json适用前提:该分析面向单个堆快照。它不支持目录级多快照分析——在 CollectionsHoldingStaleAnalysis.ts 中,
analyzeSnapshotsInDirectory()被显式覆写为抛出异常(CollectionsHoldingStaleAnalysis does not support analyzeSnapshotsInDirectory)。如果需要观察对象随时间增长的趋势,应改用unbound-collection、unbound-object等多快照分析插件。
编程 API:analyzeSnapshotFromFile()
按照 API 文档,可以通过继承自BaseAnalysis的analyzeSnapshotFromFile()以编程方式运行分析:
import {CollectionsHoldingStaleAnalysis} from '@memlab/heap-analysis'; const analysis = new CollectionsHoldingStaleAnalysis(); // 分析控制台的输出被保存到 result.analysisOutputFile const result = await analysis.analyzeSnapshotFromFile(snapshotFile);参数与返回值(定义见 BaseAnalysis.ts):
file: string——.heapsnapshot文件的绝对路径;options?: RunHeapAnalysisOptions—— 可选的运行配置,当前仅支持workDir:指定后,分析过程的中间文件、日志文件与最终输出都会写入该目录,目录不存在时 memlab 会自动创建;- 返回值
Promise<AnalyzeSnapshotResult>—— 其中analysisOutputFile字段是本次分析控制台输出的日志文件路径(类型定义见 PluginUtils.ts)。
指定工作目录的完整示例:
const analysis = new CollectionsHoldingStaleAnalysis(); const result = await analysis.analyzeSnapshotFromFile(snapshotFile, { // 若目录不存在,memlab 会自动创建 workDir: '/tmp/your/work/dir', });内部实现上,analyzeSnapshotFromFile()会先注册一个日志文件,然后以snapshot: file的伪 CLI 参数形式调用插件的process(),最后把日志文件路径作为AnalyzeSnapshotResult返回。分析结果本身(各集合的陈旧元素列表)则以终端文本或 JSON 形式写入该日志文件。
源码级剖析:扫描与 stale 判定逻辑
只关注三类集合:Map、Set、Array
分析的核心在process()回调(memlab analyze collections-with-stale的执行入口,见 源码):先通过pluginUtils.loadHeapSnapshot(options)加载并解析堆快照(同时计算 dominator 树与 retained size),再调用getCollectionsWithStaleValues()收集结果,最后按--output配置选择print()(文本)或printJson()(JSON)输出。
哪些节点被视为“集合”?由实例成员staleCollectionMapper决定:
// packages/heap-analysis/src/plugins/CollectionsHoldingStaleAnalysis.ts private staleCollectionMapper: Map<string, (node: IHeapNode) => CollectionStat> = new Map([ ['Map', processMapOrSet], ['Set', processMapOrSet], ['Array', processCollectionChildren], ]);getCollectionsWithStaleValues()遍历快照中所有可追踪节点(snapshot.nodes.forEachTraceable),仅对名称命中Map/Set/Array的节点收集统计;统计中没有任何陈旧子元素的集合会被直接丢弃(staleChildren.length > 0才入列)。从源码结构看,这一设计是刻意为之:WeakMap/WeakSet持有弱引用,不构成强引用泄漏,因此不在分析范围内。
Map/Set 需要透过 table 边找到真正的元素
Map与Set在 V8 堆快照中有一层间接结构:集合对象本身通过一条名为table的边指向内部的哈希表对象,元素是挂在该表对象上的。processMapOrSet()正是利用了这一点:
function processMapOrSet(node: IHeapNode): CollectionStat { // 取集合对象的 table 出边,指向内部哈希表 const tableEdge = utils.getEdgeByNameAndType(node, 'table'); if (!tableEdge) { return initCollectionStat(node); } return processCollectionChildren(tableEdge.toNode); }而Array的元素直接通过下标边挂在数组节点上,所以直接交给processCollectionChildren()。getEdgeByNameAndType的实现见 Utils.ts。
每个集合的统计项:CollectionStat
对每个命中的集合,分析会构建一个CollectionStat统计结构:
type CollectionStat = { collection: IHeapNode; // 集合节点本身 staleChildren: IHeapNode[]; // 处于 stale 状态的直接子元素 childrenSize: number; // 集合的直接子元素总数 staleRetainedSize: number; // stale 子元素的 retainedSize 累加值 };processCollectionChildren()的逻辑是逐条遍历集合节点的出边(node.references),对每个目标元素节点做 stale 判定并累计:
function processCollectionChildren(node: IHeapNode): CollectionStat { const stat = initCollectionStat(node); const refs = node.references; for (const ref of refs) { ++stat.childrenSize; const elementNode = ref.toNode; if ( !utils.isDetachedDOMNode(elementNode) && !utils.isDetachedFiberNode(elementNode) ) { continue; } stat.staleChildren.push(elementNode); stat.staleRetainedSize += elementNode.retainedSize; } return stat; }这里的关键是只检查集合的直接子元素,而不是集合可支配(dominated)的整个子树——这与“集合直接持有元素”的语义一致,也避免把嵌套在普通对象内部的 detached 节点误计入。
stale 判定:isDetachedDOMNode 与 isDetachedFiberNode
两个判定函数都来自@memlab/core的 utils。其共同基础是isDetached():
// packages/core/src/lib/Utils.ts function isDetached(node: IHeapNode): boolean { if (config.snapshotHasDetachedness) { return node.is_detached; // 快照带 detachedness 元数据时直接读字段 } return node.name.startsWith('Detached '); // 否则退回按节点名称前缀判断 }isDetachedFiberNode(node):node是FiberNode/Detached FiberNode且满足isDetached;isDetachedDOMNode(node):排除 Fiber 节点后,判断节点是否处于 detached 状态(例如Detached HTMLDivElement、Detached InternalNode等已从文档树摘除的 DOM 节点)。
需要留意的适用边界:源码注释明确指出,当快照未携带 detachedness 字段时,判定依赖Detached名称前缀,这类 FiberNode 若无 detachedness 字段则无法被识别(isDetachedFiberNode上方的 NOTE 注释)。
输出格式:文本与 JSON
结果统一按staleRetainedSize降序排序,且只输出前MAX_COLLECTION_STAT_ITEMS = 20个集合(常量定义在 源码第 21 行)。
文本模式(默认,print()):若没有任何命中集合,输出Congratulations! No collections holding stale objects;否则逐条打印:
Collections holding stale objects: · Array (@12345) 2.4 MB (3 out of 102 elements are stale): @12900, @12901, @13055, ...每条包含集合类型名、节点 ID、集合的 retained size(经utils.getReadableBytes人类可读化),以及“N out of M elements are stale”的陈旧比例描述;陈旧子元素 ID 最多打印 10 个,超出部分以...结尾(见 print 实现)。
JSON 模式(--output=json,printJson()):输出一个 JSON 数组,每项字段为:
| 字段 | 含义 |
|---|---|
id | 集合节点 ID |
size | 集合 retained size(可读字节) |
childrenSize | 集合直接子元素总数 |
staleChildrenSize | 其中 stale 的元素数量 |
staleChildrenIds | 全部 stale 元素节点 ID 列表 |
JSON 模式适合接入脚本或 Agent 做二次处理,例如批量提取staleChildrenIds后逐一追踪引用链。
从分析结果到定位泄漏源
collections-with-stale给出的节点 ID 可以直接衔接 memlab 的其它能力,形成一条完整的排查链路:
- 用
memlab trace --node-id=<HEAP_OBJECT_ID>追踪某个 stale 元素或集合本身的 retainer 链,看它被哪条路径钉在堆上; - 结合
memlab analyze detached-DOM、unmounted-fiber-node等单快照分析交叉验证; - 如果怀疑集合本身在持续膨胀(条目数单调增长),再切换到多快照的
memlab analyze unbound-collection用--snapshot-dir观察一段时间序列。
小结
CollectionsHoldingStaleAnalysis是 memlab 中一个目标明确的单快照堆分析插件:它仅针对Map、Set、Array三类集合,通过table边穿透 Map/Set 的内部结构,用isDetachedDOMNode/isDetachedFiberNode判定直接子元素是否“陈旧”,最终按陈旧元素 retained size 输出 Top 20 命中集合。无论是通过 CLI(memlab analyze collections-with-stale --snapshot=... [--output=json])还是编程 API(analyzeSnapshotFromFile(file, {workDir}))调用,都能为“集合滞留 detached 对象”这一高频泄漏模式提供直接的量化线索。核心实现可进一步参阅 CollectionsHoldingStaleAnalysis.ts、BaseAnalysis.ts 与 core 的 utils。
- 开发工具
- 测试
- CLI
- MCP 服务
【免费下载链接】memlab
A framework for finding JavaScript memory leaks and analyzing heap snapshots
相关推荐
memlab 的 findLeaks() API:从堆快照中定位 JavaScript 内存泄漏
memlab 的 findLeaks API:从堆快照中定位 JavaScript 内存泄漏 findLeaks 是 @memlab/api https://l
开发工具测试CLIMCP 服务给AI Agent一个浏览器:Obscura MCP服务器完整配置指南(Claude Desktop/Cursor实战)
给AI Agent一个浏览器:Obscura MCP服务器完整配置指南(Claude Desktop/Cursor实战) Obscura 是一个用 Rust 编
网页爬虫后端MCP 服务浏览器控制memlab API 详解:使用 findLeaksBySnapshotFilePaths() 基于已有堆快照文件定位 JavaScript 内存泄漏
memlab API 详解:使用 findLeaksBySnapshotFilePaths 基于已有堆快照文件定位 JavaScript 内存泄漏 导读 fin
开发工具测试CLIMCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考