深入解析 Puppeteer JSCoverageOptions:JavaScript 代码覆盖率采集的四个关键开关
导读
Puppeteer 内置的 Coverage 能力可以让你在真实 Chromium 页面中量化 JavaScript 的执行情况——哪些脚本被加载过、哪些代码片段真正运行过。而 JSCoverageOptions 正是 page.coverage.startJSCoverage() 的配置入口:通过四个可选的布尔开关,你可以控制覆盖率数据是否随导航重置、是否包含无 URL 的匿名脚本、是否附带 V8 原始脚本记录,以及按块级还是函数级粒度采集。读完本文,你将能准确理解每个选项的作用原理与默认值,并结合 Puppeteer 源码与官方测试用例,写出可以真正落地的前端资源使用率分析脚本。
JSCoverageOptions 是什么
JSCoverageOptions 是 Puppeteer 公开 API 中一个纯配置接口,类型定义如下:
export interface JSCoverageOptions {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
includeRawScriptCoverage?: boolean;
useBlockCoverage?: boolean;
}
它不包含任何方法,唯一的用途是作为 Coverage.startJSCoverage(options) 方法的可选入参,控制 JS 覆盖率采集行为。完整定义见 packages/puppeteer-core/src/cdp/Coverage.ts。
从源码结构看,该接口由 CDP(Chrome DevTools Protocol)层的 JSCoverage 类 消费:start() 接收这些选项,将它们拆包后保存在内部字段中,并据此发送不同的 CDP 指令。page.coverage.startJSCoverage() 是公共入口,内部代理到 JSCoverage.start():
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
return await this.#jsCoverage.start(options);
}
对应文档见 Coverage.startJSCoverage()。
四个配置项逐一拆解
下表汇总了四个属性的完整语义,均可在 docs/api/puppeteer.jscoverageoptions.md 中查到:
| 属性 | 修饰符 | 类型 | 作用 | 实际默认值 |
|---|---|---|---|---|
resetOnNavigation |
optional | boolean | 每次页面导航时是否重置已采集的覆盖率 | true |
reportAnonymousScripts |
optional | boolean | 是否报告页面运行时生成的无 URL 匿名脚本 | false |
includeRawScriptCoverage |
optional | boolean | 结果中是否附带 V8 原始脚本覆盖率条目 | false |
useBlockCoverage |
optional | boolean | 按块级采集(默认)还是按函数级采集 | true |
需要特别说明:API 文档表格中「Default」列为空,但默认值并非未定义。在 JSCoverage.start() 的实现里,四个选项被显式解构并赋予默认值:
const {
resetOnNavigation = true,
reportAnonymousScripts = false,
includeRawScriptCoverage = false,
useBlockCoverage = true,
} = options;
同时,startJSCoverage 的官方注释也明确给出默认集合为 resetOnNavigation : true, reportAnonymousScripts : false, includeRawScriptCoverage : false, useBlockCoverage : true。因此即使完全不带参数调用 startJSCoverage(),采集器也会以上述默认行为运行。
resetOnNavigation:跨导航的采样策略
该选项决定当页面发生导航(例如跳转到新页面)时,已记录的脚本 URL 与源码缓存是否被清空。
从实现看,导航会触发 CDP 的 Runtime.executionContextsCleared 事件。在 JSCoverage 内部,只有当 #resetOnNavigation 为 true 时,事件回调才会清空 #scriptURLs 与 #scriptSources 两个 Map:
#onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) {
return;
}
this.#scriptURLs.clear();
this.#scriptSources.clear();
}
测试用例直观展示了它的效果(见 test/src/coverage.test.ts):
it('should NOT report scripts across navigations when enabled', async () => {
await page.coverage.startJSCoverage(); // 默认开启 resetOnNavigation
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
await page.goto(server.EMPTY_PAGE);
const coverage = await page.coverage.stopJSCoverage();
expect(coverage).toHaveLength(0);
});
- 保持默认
true:适合只关心当前页面(通常是最后一次导航后加载的页面)执行情况的场景,这也是 Puppeteer 官方推荐的使用方式——先startJSCoverage()再goto,只统计目标页面自身。 - 设为
false:适合需要跨导航持续累计采集(例如追踪一个完整的用户操作流程中多个页面脚本执行量)的场景。
reportAnonymousScripts:要不要管 eval 与 new Function
所谓匿名脚本,是指没有关联 URL、由页面动态创建出来的脚本——典型来源是 eval() 或 new Function()。默认 false 意味着这些脚本不会出现在覆盖率结果中;设为 true 后它们会被纳入统计。
实现层面有两个关键过滤点。其一,脚本解析阶段(#onScriptParsed)会丢弃没有 URL 且未开启本选项的脚本:
if (!event.url && !this.#reportAnonymousScripts) {
return;
}
其二,停止采集阶段(stop()),当某个条目缺少 URL 但 reportAnonymousScripts 为真时,Puppeteer 会为其生成形如 debugger://VM<scriptId> 的合成 URL:
let url = this.#scriptURLs.get(entry.scriptId);
if (!url && this.#reportAnonymousScripts) {
url = 'debugger://VM' + entry.scriptId;
}
需要注意一个例外:如果匿名脚本带有魔法注释 //# sourceURL=xxx.js,V8 会优先将该名字作为 URL,Puppeteer 也会随之使用 xxx.js 而非 debugger://VM。
官方测试对此有针对性验证(test/src/coverage.test.ts):关闭时 eval.html 只报告 1 个脚本(匿名脚本被忽略);开启 {reportAnonymousScripts: true} 后过滤掉 debugger:// 前缀条目,仍然只得到 1 个,说明多余的匿名脚本确实被补报进来。此外 Puppeteer 注入页面内部的脚本也会被显式排除(见 #onScriptParsed 中对 PuppeteerURL.isPuppeteerURL 的判断),因此开启本选项不会引入框架自身的噪声,这一点同样有测试覆盖(test/src/coverage.test.ts)。
includeRawScriptCoverage:是否保留 V8 原始脚本记录
默认 false。当置为 true 时,每个覆盖率条目会额外携带 rawScriptCoverage 字段,其类型为 CDP Protocol.Profiler.ScriptCoverage(见 JSCoverageEntry),即 V8 探针返回的、尚未经 Puppeteer 归并处理的原始记录。
这一选项在启动阶段直接影响 CDP 指令的参数(JSCoverage.start()):
await Promise.all([
this.#client.send('Profiler.enable'),
this.#client.send('Profiler.startPreciseCoverage', {
callCount: this.#includeRawScriptCoverage,
detailed: useBlockCoverage,
}),
this.#client.send('Debugger.enable'),
this.#client.send('Debugger.setSkipAllPauses', {skip: true}),
]);
当 includeRawScriptCoverage 为 true 时,callCount 参数被置为 true,意味着 V8 会额外统计每个位置的调用次数(call count),这也是获取逐函数调用频率的前提。停止采集时(stop()),Puppeteer 依据该选项决定是否把原始 entry 一并放入返回结果:
if (!this.#includeRawScriptCoverage) {
coverage.push({url, ranges, text});
} else {
coverage.push({url, ranges, text, rawScriptCoverage: entry});
}
测试同样验证了这一点(test/src/coverage.test.ts):默认返回的条目 rawScriptCoverage 为 undefined;开启后该字段有值。
适用建议:绝大多数使用率统计场景并不需要它;只有当你想拿到 V8 底层的逐函数范围、调用计数进行自定义深度分析(例如精确的按行/按函数命中次数)时才开启,代价是返回数据量显著增大。
useBlockCoverage:块级还是函数级采集粒度
这是采集精度的开关:true(默认)时按块级(block)采集,可区分同一函数内部 if 分支哪一段被执行;false 时退回函数级(function),一个函数只要被调用过一次,其整体就算作已覆盖。
从源码可见,它直接映射到 Profiler.startPreciseCoverage 的 detailed 参数(见上文代码),detailed: true 表示精确到块/行的细粒度记录。
官方测试用同一个页面对比了两种粒度(test/src/coverage.test.ts)。测试页面 ranges.html 含有一行关键代码:console.log('used!');if(true===false)console.log('unused!');
- 块级(默认):由于
if(true===false)中的分支从未执行,V8 可将其从已覆盖区间中剔除,返回的第二个 range 只覆盖到console.log('used!');if(true===false); - 函数级(
useBlockCoverage: false):整行代码作为函数体被视为已执行,第二个 range 把console.log('unused!');也包含了进去,覆盖区间明显更大、粒度更粗。
由此可以得出清晰的选型结论:追求精确的按行/分支级使用率分析(如定位可删除的死代码)应保持默认 true;若你的分析工具或下游处理只关心「函数是否被调用」这一粗粒度结论,或希望减少 V8 采集开销,可以显式传入 {useBlockCoverage: false}。
综合使用示例:测量页面初始加载代码使用率
将上述选项与 CSS 覆盖率结合,即可得到 Puppeteer 官方文档推荐的端到端统计脚本(语义来源:Coverage 类 的示例;四选项的带参用法为下面的扩展写法):
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 按自定义选项启动 JS 覆盖率采集
await Promise.all([
page.coverage.startJSCoverage({
resetOnNavigation: true, // 只统计 goto 后的当前页面
reportAnonymousScripts: false, // 忽略 eval/new Function 生成的匿名脚本
includeRawScriptCoverage: false, // 不需要 V8 原始记录,节省内存
useBlockCoverage: true, // 块级精度,能识别未执行的分支
}),
page.coverage.startCSSCoverage(), // 同时开启 CSS 覆盖率
]);
await page.goto('https://example.com');
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage(),
]);
let totalBytes = 0;
let usedBytes = 0;
const coverage = [...jsCoverage, ...cssCoverage];
for (const entry of coverage) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
usedBytes += range.end - range.start - 1;
}
}
console.log(`Bytes used: ${((usedBytes / totalBytes) * 100).toFixed(2)}%`);
await browser.close();
需要注意:
startJSCoverage与stopJSCoverage必须成对使用,且在同一个Coverage实例上不可重复开启。源码中通过断言(assert(!this.#enabled, 'JSCoverage is already enabled'))防止重复启动,见 JSCoverage.start() 与 stop()。- 每一条结果都形如
JSCoverageEntry,它继承自CoverageEntry,固定包含url、text与ranges三个字段:url:脚本地址;带sourceURL注释的内联脚本会使用其声明的名字(测试见 test/src/coverage.test.ts),开启reportAnonymousScripts后匿名脚本为debugger://VM...;text:脚本完整源码;ranges:去重叠后、已执行区间的{start, end}字符偏移数组(由 convertToDisjointRanges 用扫描线算法把 V8 返回的嵌套函数区间归并为互不重叠的区间)。ranges为空数组代表脚本被加载但完全没有执行(测试见 test/src/coverage.test.ts)。
- 若需输出为 Istanbul(
istanbuljs)生态可消费的格式,官方文档建议配合puppeteer-to-istanbul使用,详见 Coverage 类文档 的 Remarks 一节。
各选项在启动链路上的落点
为了便于从整体上把握,可以把四个选项的底层映射关系整理如下,全部可在 JSCoverage.start() 中逐一找到对应代码:
| 选项 | 内部字段 | 生效位置 / CDP 落点 |
|---|---|---|
resetOnNavigation |
#resetOnNavigation |
Runtime.executionContextsCleared 事件回调是否清空缓存 |
reportAnonymousScripts |
#reportAnonymousScripts |
Debugger.scriptParsed 的过滤条件 + 停止时为无 URL 脚本生成 debugger://VM 地址 |
includeRawScriptCoverage |
#includeRawScriptCoverage |
Profiler.startPreciseCoverage 的 callCount 参数 + 结果是否附带 rawScriptCoverage |
useBlockCoverage |
无独立字段,直接透传 | Profiler.startPreciseCoverage 的 detailed 参数(块级 vs 函数级) |
采集结束后,stop() 会依次发送 Profiler.takePreciseCoverage、Profiler.stopPreciseCoverage、Profiler.disable 与 Debugger.disable 完成收尾,并把 V8 返回的嵌套函数区间经 convertToDisjointRanges 归并为连续的 ranges,最终以 JSCoverageEntry[] 形式返回给调用方。
总结与选型建议
JSCoverageOptions 用四个布尔开关把 Puppeteer 的 JS 覆盖率采集做成了高度可配置的能力。实践中最常用的组合是全部使用默认值:官方默认已经针对「测量单个页面初始执行比例」这一主流场景做了优化——resetOnNavigation: true 保证统计起点干净,useBlockCoverage: true 提供块级精度,reportAnonymousScripts: false 排除 eval 噪声,includeRawScriptCoverage: false 保持返回结构精简。
当你需要更复杂的行为时,按需开启即可:
- 需要覆盖多页导航流程 → 显式设置
resetOnNavigation: false; - 需要把 eval /
new Function生成的动态代码纳入统计 → 开启reportAnonymousScripts,并留意debugger://VM前缀与//# sourceURL特例; - 需要 V8 原始记录做调用计数等深度分析 → 开启
includeRawScriptCoverage; - 只关心函数级是否执行、想降低采集开销 → 设置
useBlockCoverage: false。
配合 Coverage.ts 实现 与 coverage.test.ts 测试,你可以为每一个选项找到确切的代码路径与行为佐证,从而在真实项目中做出有依据的选择。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00