Puppeteer Coverage.startJSCoverage() 详解:页面 JavaScript 执行覆盖率采集与实战
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
Coverage.startJSCoverage()是 Puppeteer 中用于开启页面 JavaScript 执行覆盖率收集的核心 API。它基于 Chrome DevTools Protocol(CDP)的 Profiler 与 Debugger 域实现,让开发者可以精确统计页面加载后"哪些脚本被执行、哪些代码字节真正用上了",从而支撑前端性能优化、无用代码剔除与自动化测试质量评估。读完本文,你将掌握该方法的完整签名、全部可配置参数及其默认行为、匿名脚本(eval / new Function)的处理细节,并能写出计算脚本利用率百分比的完整可运行代码。
本文以仓库 API 文档 docs/api/puppeteer.coverage.startjscoverage.md 为核心骨架,并结合 Coverage 类源码 与 coverage 测试套件 展开实现级讲解。
方法签名与调用入口
startJSCoverage()定义在Coverage类上,其 TypeScript 签名如下:
class Coverage { startJSCoverage(options?: JSCoverageOptions): Promise<void>; }在真实 API 文档(docs/api/puppeteer.coverage.md)中,Coverage类被描述为"提供收集页面中被使用的 JavaScript 与 CSS 部分信息的方法"。它通过page.coverage属性暴露,Page 的抽象声明位于 packages/puppeteer-core/src/api/Page.ts#L1038,如下:
abstract get coverage(): Coverage;也就是说,日常用法是page.coverage.startJSCoverage(...)。该方法返回Promise<void>——Promise 在覆盖率收集真正启动后才会 resolve,因此你可以安全地await之后再进行页面跳转,确保不会漏掉首批脚本的执行信息。
参数详解:JSCoverageOptions 的四个可选项
options的类型为 JSCoverageOptions 接口,全部字段都是可选(optional)的。根据 startJSCoverage 的 API 文档 与源码注释,各参数的默认值合计为:
默认值:
resetOnNavigation: true,reportAnonymousScripts: false,includeRawScriptCoverage: false,useBlockCoverage: true
resetOnNavigation(默认 true):导航时是否重置采集状态
接口定义见 docs/api/puppeteer.jscoverageoptions.md:Whether to reset coverage on every navigation.
置为true时,每当页面发生导航(触发Runtime.executionContextsCleared)就会清空已记录的脚本 URL 与源码表,只统计当前页面生命周期内的执行情况;置为false则可以跨页面(SPA 内多次导航 / 多页跳转)持续累积覆盖率。
在源码 Coverage.ts 中,清空逻辑由内部方法#onExecutionContextsCleared实现:
#onExecutionContextsCleared(): void { if (!this.#resetOnNavigation) { return; } this.#scriptURLs.clear(); this.#scriptSources.clear(); }对应测试可见 test/src/coverage.test.ts:describe('resetOnNavigation')下验证了开启时"不应跨导航上报脚本"的行为。
reportAnonymousScripts(默认 false):是否上报匿名脚本
匿名脚本(anonymous scripts)指没有关联 URL 的脚本——即页面中通过eval或new Function动态创建的代码。当该选项为true时,这类脚本会被纳入上报,且其 URL 以debugger://VM开头(如debugger://VM123,其中 123 是 V8 的 scriptId);除非脚本中存在 magic//# sourceURL注释,此时将使用该注释声明的 URL 作为脚本标识。
源码中 URL 兜底逻辑位于JSCoverage.stop()(Coverage.ts):
let url = this.#scriptURLs.get(entry.scriptId); if (!url && this.#reportAnonymousScripts) { url = 'debugger://VM' + entry.scriptId; }同时,脚本解析阶段#onScriptParsed(Coverage.ts)会先丢弃无 URL 且未开启该选项的脚本,并跳过 Puppeteer 注入的内部脚本:
// Ignore puppeteer-injected scripts if (PuppeteerURL.isPuppeteerURL(event.url)) { return; } // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. if (!event.url && !this.#reportAnonymousScripts) { return; }该行为的测试证据同样充分:test 用例"should ignore eval() scripts by default"与"should not ignore eval() scripts if reportAnonymousScripts is true",以及"should ignore pptr internal scripts if reportAnonymousScripts is true"(test/src/coverage.test.ts)。
includeRawScriptCoverage(默认 false):是否附带 V8 原始覆盖条目
置为true时,每个 JSCoverageEntry 会额外携带rawScriptCoverage字段(类型为Protocol.Profiler.ScriptCoverage),其中包含 V8 给出的函数级原始覆盖数据(如每个函数的 ranges 与执行计数),便于做更深度的分析;默认false时只输出 Puppeteer 整理好的{ url, ranges, text }结构。
此开关同时决定启动时传给 CDP 的callCount参数——见下文"底层实现"小节。测试覆盖可见 test/src/coverage.test.ts:开启/关闭该字段时的包含与否均被断言。
useBlockCoverage(默认 true):块级还是函数级覆盖
- 为
true(默认):按代码块粒度收集覆盖信息,能识别if分支、条件表达式等更细粒度的未执行路径; - 为
false:退化为函数级粒度,V8 只报告整个函数是否被调用,粒度更粗、开销更低。
测试中同样有对应验证,如"should report right ranges for 'per function' scope"(test/src/coverage.test.ts)。
底层实现:startJSCoverage 内部发生了什么
Coverage.startJSCoverage()的实现非常薄,它把工作委托给内部类JSCoverage(packages/puppeteer-core/src/cdp/Coverage.ts):
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { return await this.#jsCoverage.start(options); }而JSCoverage.start()(Coverage.ts)的核心步骤可以拆解为:
- 状态守卫:通过
assert(!this.#enabled, 'JSCoverage is already enabled')防止重复开启;重复调用会直接抛出断言错误。相应地,重复stop()会抛出'JSCoverage is not enabled'。 - 解构默认值:把四个选项分别落库为实例字段,并将
#enabled置为true、清空脚本 URL / 源码映射表、建立可自动清理的DisposableStack事件订阅。 - 注册两个 CDP 事件监听:
Debugger.scriptParsed:当页面解析出(静态<script>、动态脚本、Web Worker 脚本等)任何脚本时触发,回调中调用Debugger.getScriptSource拉取源码并存表;Runtime.executionContextsCleared:导航/上下文销毁时触发,与resetOnNavigation联动清空缓存。
- 并发发送四条 CDP 指令完成能力开启:
await Promise.all([ this.#client.send('Profiler.enable'), this.#client.send('Profiler.startPreciseCoverage', { callCount: this.#includeRawScriptCoverage, // 对应 includeRawScriptCoverage detailed: useBlockCoverage, // 对应 useBlockCoverage }), this.#client.send('Debugger.enable'), this.#client.send('Debugger.setSkipAllPauses', {skip: true}), ]);这里值得注意的实现细节是:callCount恰好映射到includeRawScriptCoverage,而detailed映射到useBlockCoverage,与上文选项含义一一对应。此外Debugger.setSkipAllPauses被置为skip: true,用于防止页面中的debugger语句打断采集流程(测试中专门有"should not hang when there is a debugger statement"用例,见 test/src/coverage.test.ts)。
停止采集时的数据整理
与分析配套的stopJSCoverage()(详见 docs/api/puppeteer.coverage.stopjscoverage.md)返回Promise<JSCoverageEntry[]>。JSCoverage.stop()(Coverage.ts)会并发调用Profiler.takePreciseCoverage、Profiler.stopPreciseCoverage、Profiler.disable、Debugger.disable,随后把 V8 返回的函数嵌套区间扁平化,经convertToDisjointRanges(Coverage.ts)合并为互不相交的{ start, end }覆盖区间——该函数用"括号序列排序 + 扫描线"算法把多个函数/块的重叠范围规约为最终答案。
每个结果条目JSCoverageEntry(docs/api/puppeteer.jscoverageentry.md)继承自CoverageEntry,含三个核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
url | string | 脚本的 URL(匿名脚本为debugger://VM<scriptId>或 sourceURL) |
text | string | 脚本完整源码文本 |
ranges | Array<{start: number; end: number}> | 已执行代码覆盖的字节区间(相对text的偏移) |
rawScriptCoverage(可选) | Protocol.Profiler.ScriptCoverage | 开启includeRawScriptCoverage后附带的 V8 原始数据 |
关于 sourceURL 的一个"反直觉"点
stopJSCoverage的 Remarks(docs/api/puppeteer.coverage.stopjscoverage.md)特别说明:JavaScript Coverage 默认不包含匿名脚本,但带有 sourceURL 的脚本会被上报。也就是说,即便reportAnonymousScripts: false,只要动态脚本通过//# sourceURL=...注释声明了标识,它依然会进入报告(V8 会将其视为"有 URL 的脚本")。测试"should report sourceURLs"(test/src/coverage.test.ts)正是对这一行为的验证。
完整实战:计算页面初始代码利用率
API 文档(docs/api/puppeteer.coverage.md)给出了一个同时测量 JS 与 CSS 覆盖率并换算百分比的标准示例,可直接运行:
// 同时开启 JavaScript 与 CSS 覆盖率采集 await Promise.all([ page.coverage.startJSCoverage(), page.coverage.startCSSCoverage(), ]); // 跳转到目标页面,等待页面代码被执行 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}%`);要点解读:
startJSCoverage()不传参数即等价于传入全部默认值(resetOnNavigation: true等),适合只测单页加载场景;usedBytes的计算规则是区间长度减 1,用于消除相邻区间合并时的重叠边界误差,与官方示例保持一致;- 想单独评估"首页 JS 中有多少被真正执行",可把 CSS 部分移除,只保留
jsCoverage的报告与计算逻辑。
常见调参与典型场景建议
针对不同需求,可以按如下方式组合参数(均可选填):
// 场景一:SPA 应用连续导航后做整体统计(跨导航累积) await page.coverage.startJSCoverage({ resetOnNavigation: false }); // 场景二:希望把 eval/new Function 产生的动态代码也纳入统计 await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); // 场景三:需要拿到 V8 原始函数级数据做自定义分析 await page.coverage.startJSCoverage({ includeRawScriptCoverage: true }); // 场景四:只关心"函数是否被调用",降低采集开销 await page.coverage.startJSCoverage({ useBlockCoverage: false }); // 也可以自由组合 await page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: true, includeRawScriptCoverage: true, });从测试套件看,仓库对如下边界情况均有验证,实战中可作为预期参考:
- 覆盖"应报告多个脚本""应报告无覆盖脚本"与"应报告正确区间"(test/src/coverage.test.ts);
- 条件表达式(
conditionals)下的块级覆盖正确性(test/src/coverage.test.ts); - 页面存在
debugger语句时不会导致采集挂起(test/src/coverage.test.ts); includeRawScriptCoverage开/关时原始字段的包含与缺失(test/src/coverage.test.ts)。
注意事项与易踩的坑
结合 Coverage 类源码 的实现,以下几点值得特别留意:
- 不可重复开启:同一
Coverage实例上连续调用两次startJSCoverage()会因assert抛出'JSCoverage is already enabled';必须先stopJSCoverage()再重新开启。 - 结束后及时停止:采集期间 Puppeteer 保持 Profiler/Debugger 域开启并有事件订阅,会带来一定运行时开销,采集完应立即调用
stopJSCoverage()释放。 - 脚本源码获取可能失败:
#onScriptParsed中若页面已跳走,Debugger.getScriptSource可能失败,此时会记入 error 日志并跳过该脚本(Coverage.ts),这是正常降级而非缺陷。 - 匿名脚本的两套规则容易混淆:
reportAnonymousScripts: false(默认)时,eval/new Function产生的无 URL 脚本一律不上报;- 但只要动态脚本带
//# sourceURL,就会被视为"有 URL",两种开关下都会被上报; reportAnonymousScripts: true时,其余匿名脚本会以debugger://VM<id>形式进入报告。
- Puppeteer 自身注入的脚本会被忽略,避免内部工具脚本污染统计结果。
延伸阅读
- Coverage 类总览与综合示例
- startCSSCoverage / stopCSSCoverage(CSS 覆盖率,配合使用)
- JSCoverageOptions 接口字段定义
- JSCoverageEntry 返回条目结构
- Coverage 类的 CDP 实现源码
- 覆盖率的完整行为测试套件
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考