首页
/ 深入解析 Puppeteer JSCoverageOptions:JavaScript 代码覆盖率采集的四个关键开关

深入解析 Puppeteer JSCoverageOptions:JavaScript 代码覆盖率采集的四个关键开关

2026-09-07 13:50:10作者:滑思眉Philip

导读

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 内部,只有当 #resetOnNavigationtrue 时,事件回调才会清空 #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}),
]);

includeRawScriptCoveragetrue 时,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):默认返回的条目 rawScriptCoverageundefined;开启后该字段有值。

适用建议:绝大多数使用率统计场景并不需要它;只有当你想拿到 V8 底层的逐函数范围、调用计数进行自定义深度分析(例如精确的按行/按函数命中次数)时才开启,代价是返回数据量显著增大。

useBlockCoverage:块级还是函数级采集粒度

这是采集精度的开关:true(默认)时按块级(block)采集,可区分同一函数内部 if 分支哪一段被执行;false 时退回函数级(function),一个函数只要被调用过一次,其整体就算作已覆盖。

从源码可见,它直接映射到 Profiler.startPreciseCoveragedetailed 参数(见上文代码),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();

需要注意:

  1. startJSCoveragestopJSCoverage 必须成对使用,且在同一个 Coverage 实例上不可重复开启。源码中通过断言(assert(!this.#enabled, 'JSCoverage is already enabled'))防止重复启动,见 JSCoverage.start()stop()
  2. 每一条结果都形如 JSCoverageEntry,它继承自 CoverageEntry,固定包含 urltextranges 三个字段:
    • url:脚本地址;带 sourceURL 注释的内联脚本会使用其声明的名字(测试见 test/src/coverage.test.ts),开启 reportAnonymousScripts 后匿名脚本为 debugger://VM...
    • text:脚本完整源码;
    • ranges:去重叠后、已执行区间的 {start, end} 字符偏移数组(由 convertToDisjointRanges 用扫描线算法把 V8 返回的嵌套函数区间归并为互不重叠的区间)。ranges 为空数组代表脚本被加载但完全没有执行(测试见 test/src/coverage.test.ts)。
  3. 若需输出为 Istanbul(istanbuljs)生态可消费的格式,官方文档建议配合 puppeteer-to-istanbul 使用,详见 Coverage 类文档 的 Remarks 一节。

各选项在启动链路上的落点

为了便于从整体上把握,可以把四个选项的底层映射关系整理如下,全部可在 JSCoverage.start() 中逐一找到对应代码:

选项 内部字段 生效位置 / CDP 落点
resetOnNavigation #resetOnNavigation Runtime.executionContextsCleared 事件回调是否清空缓存
reportAnonymousScripts #reportAnonymousScripts Debugger.scriptParsed 的过滤条件 + 停止时为无 URL 脚本生成 debugger://VM 地址
includeRawScriptCoverage #includeRawScriptCoverage Profiler.startPreciseCoveragecallCount 参数 + 结果是否附带 rawScriptCoverage
useBlockCoverage 无独立字段,直接透传 Profiler.startPreciseCoveragedetailed 参数(块级 vs 函数级)

采集结束后,stop() 会依次发送 Profiler.takePreciseCoverageProfiler.stopPreciseCoverageProfiler.disableDebugger.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 测试,你可以为每一个选项找到确切的代码路径与行为佐证,从而在真实项目中做出有依据的选择。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388