Puppeteer 页面性能追踪指南:深入解析 Tracing.start() 的用法、配置与底层原理
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
导读
Puppeteer 通过页面对象暴露的page.tracing属性提供了一套基于 Chrome DevTools Protocol(CDP)的性能审计接口,可用于在自动化浏览器会话中记录页面加载、脚本执行、渲染与布局等完整时间线数据。本文以 Tracing.start() 方法文档 为核心,串联 Tracing 类文档、TracingOptions 配置文档 与 Tracing.stop() 方法文档,并结合 cdp/Tracing.ts 源码 与 tracing 测试用例,讲解如何启动一次页面追踪、定制追踪类别与缓冲区大小、把结果写入文件或在内存中获取,最终产出可被 Chrome DevTools 或 Timeline Viewer 打开分析的 trace 文件。读完本文,你将能在自己的 Puppeteer 脚本中熟练进行端到端的性能追踪与结果解读。
Tracing.start() 方法:一次追踪的开始
Tracing.start() 方法文档 给出的方法签名如下:
class Tracing { start(options?: TracingOptions): Promise<void>; }它用于"为当前页面启动一次追踪"(Starts a trace for the current page),接受一个可选的TracingOptions参数,返回一个Promise<void>。真正产出的 trace 数据由后续调用的stop()返回或写入磁盘文件。
与绝大多数 Puppeteer API 一致,该方法必须与 Tracing.stop() 成对使用,形成一个完整的"开始录制 → 驱动页面行为 → 结束录制"闭环。
只允许一个活动追踪
原文档的 Remarks 中特别强调了一个关键约束:每个浏览器同一时刻只能有一个活动的追踪(Only one trace can be active at a time per browser)。这一点在 Tracing 类源码 中由一条断言落实:
async start(options: TracingOptions = {}): Promise<void> { assert( !this.#recording, 'Cannot start recording trace while already recording trace.', ); // ... this.#recording = true; }#recording是 Tracing 实例内部的私有状态位,在进入录制时置为true,stop()之后才复位。若你在上一次追踪尚未结束时再次调用start(),就会抛出Cannot start recording trace while already recording trace.错误。
需要特别指出的是,这条限制是"per browser"(全局性)的,而非仅仅"per page"。因为所有页面共用同一个浏览器进程的 trace buffer,因此即便在不同的page上分别调用tracing.start(),第二个调用同样会失败。测试用例 专门验证了这一点——先在 page A 上启动追踪,再在新建的 page B 上启动追踪,并断言第二次调用抛出了错误:
it('should throw if tracing on two pages', async () => { await page.tracing.start({path: outputFile}); const newPage = await browser.newPage(); let error!: Error; await newPage.tracing.start({path: outputFile}).catch(error_ => { return (error = error_); }); await newPage.close(); expect(error).toBeTruthy(); await page.tracing.stop(); });因此,在编写多页面并发脚本时,务必确认在启动新追踪前已停止旧的追踪,否则会收到断言错误。
深入解读 TracingOptions 配置项
Tracing.start()的全部行为都由可选的 TracingOptions 对象控制。该接口在源码中定义于 cdp/Tracing.ts,共有四个字段,完整参数如下:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
path | string | 否 | 无 | 把 trace 写入的磁盘文件路径。若省略,trace 不会落盘,但仍可通过tracing.stop()的返回值以Uint8Array形式拿到 |
screenshots | boolean | 否 | false | 是否在 trace 中捕获(页面)截图帧 |
categories | string[] | 否 | 见下方源码列表 | 需要包含/排除的追踪类别;排除时以-作为前缀(例如-toplevel) |
bufferSize | number | 否 | Chromium 默认 200 MB(200,000 KB) | 追踪缓冲区大小,单位是 KB;不传或传0时使用 Chromium 默认值 |
path:落盘还是留内存?
path决定 trace 数据的去向:
- 指定
path:追踪结束后,数据会异步写入该文件,典型为 JSON 格式的 trace 文件(Chromium trace 事件的 JSON 序列化产物,测试代码中直接用JSON.parse(fs.readFileSync(outputFile))读取校验,见 测试用例); - 省略
path:stop()仍会返回包含全部 trace 数据的Uint8Array(见 TracingOptions 文档 中 path 字段的说明),你可以在内存中直接传给分析工具或自行持久化。
screenshots:让时间线带画面
screenshots: true会在 trace 时间线中嵌入截图帧,还原页面视觉变化过程。其实现原理是:当该选项开启时,源码会向 categories 列表追加一个disabled-by-default-devtools.screenshot类别(cdp/Tracing.ts):
if (screenshots) { categories.push('disabled-by-default-devtools.screenshot'); }所以它本质上是"默认类别 + 截图类别"的组合。注意:源码这里直接push到categories数组。若你在启动时同时显式传入了categories,截图类别会追加到你所提供的类别之上。因为默认是false,所以默认生成的 trace 不包含截图帧,体积更小。
categories:精确裁剪追踪内容
categories用于精细控制到底记录哪些类别的事件。它的语法规则如下:
- 普通字符串 =包含该类别;
- 以
-开头的字符串(如-toplevel)=排除该类别。
当完全省略该参数时,将启用源码中预置的默认类别集合(cdp/Tracing.ts):
const defaultCategories = [ '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', 'disabled-by-default-devtools.timeline.frame', 'toplevel', 'blink.console', 'blink.user_timing', 'latencyInfo', 'disabled-by-default-devtools.timeline.stack', 'disabled-by-default-v8.cpu_profiler', ];这一组类别兼顾了 Chromium 渲染主线程时间线(devtools.timeline、disabled-by-default-devtools.timeline.frame、disabled-by-default-devtools.timeline.stack)、V8 执行与 CPU 剖析(v8.execute、disabled-by-default-v8.cpu_profiler)、页面控制台与用户计时(blink.console、blink.user_timing)、帧延迟(latencyInfo)以及顶层事件(toplevel)。开头的-*配合后续的显式白名单,实现了"默认全关、按需放开"的效果。
在真正发送给浏览器的 CDP 命令之前,源码会对传入类别做一次拆分处理(cdp/Tracing.ts):
const excludedCategories = categories .filter(cat => { return cat.startsWith('-'); }) .map(cat => { return cat.slice(1); }); const includedCategories = categories.filter(cat => { return !cat.startsWith('-'); });也就是说,Puppeteer 把categories数组按前缀拆成excludedCategories与includedCategories两组,再分别传给 CDP 的traceConfig。这也解释了为什么你要排除一个类别时,必须写成-toplevel而不是单纯列出toplevel。
测试用例 验证了自定义类别的实际效果:只传['-*', 'disabled-by-default-devtools.timeline.frame']时,生成的 trace 中不包含toplevel类别的事件;而使用默认类别时,trace 中确实包含toplevel事件(见 L56-L71)。这为理解类别的包含/排除语义提供了直观的运行证据。
bufferSize:权衡窗口与内存
bufferSize控制 Chromium 内部 trace 缓冲区的大小,单位为KB。按照文档与源码注释,当该值未指定或为0时,Chromium 会采用其内置默认值200 MB(200,000 KB)。缓冲区越小,录制过程中一旦溢出就可能丢弃早期事件;缓冲区越大则能覆盖更长的录制窗口,但也会占用更多内存。
该选项会被原样透传到 CDP 的traceConfig.traceBufferSizeInKb。测试用例通过sinon对 CDP session 的send方法做 spy,验证传入bufferSize: 10时底层确实收到了traceBufferSizeInKb: 10(测试用例)。这直接印证了参数从前端 API 到协议层的完整传递链路。
底层协议链路:start() 内部做了什么
理解了参数之后,我们来看 Tracing.start() 源码 的完整执行流程。除前文已覆盖的断言、默认类别与参数拆分外,关键的 CDP 通信只有一条命令:
this.#path = path; this.#recording = true; await this.#client.send('Tracing.start', { transferMode: 'ReturnAsStream', traceConfig: { excludedCategories, includedCategories, traceBufferSizeInKb: bufferSize, }, });值得注意的实现细节是transferMode: 'ReturnAsStream'。Puppeteer 让浏览器以**协议流(Protocol Stream)**的形式回传 trace 数据,而不是一次性塞进单个 CDP 响应里,这样即使 trace 体积很大也不会撑爆消息通道。随后stop()阶段会通过流式读取来消费这些数据(详见 cdp/Tracing.ts 的 stop 实现):
this.#client.once('Tracing.tracingComplete', async event => { try { assert(event.stream, 'Missing "stream"'); const readable = await getReadableFromProtocolStream(this.#client, event.stream); const typedArray = await getReadableAsTypedArray(readable, this.#path, this.#logger); contentDeferred.resolve(typedArray ?? undefined); } catch (error) { /* ... */ } }); await this.#client.send('Tracing.end'); this.#recording = false; return await contentDeferred.valueOrThrow();整个过程为:start()先发Tracing.start→ 页面继续执行你要审计的动作 →stop()发送Tracing.end→ 浏览器触发Tracing.tracingComplete事件并携带stream句柄 → Puppeteer 把协议流转成可读流,再按需写入path文件或整理为Uint8Array返回。
Tracing 对象在架构中的位置
Tracing 类本身构造于 CDP session 之上(constructor(client: CDPSession, logger: Logger),见 cdp/Tracing.ts),其构造函数被标记为@internal——第三方代码不应直接构造或继承 Tracing(Tracing 类文档 也明确说明这一点),统一通过页面实例上的page.tracing访问器获取。
在 CDP 协议实现中,cdp/Page.ts 持有#tracing私有成员,在 L184 处以当前 session 与 logger 构造 Tracing,并在 L308 处通过updateClient(newSession)在页面 session 切换(例如跨 OOPIF 或重连)时同步内部 CDP client,最后在 L614 以属性 getter 形式对外暴露tracing。值得注意的是,WebDriver BiDi 协议侧的页面实现同样提供只读的tracing成员(bidi/Page.ts),并在其 L130 使用 frame 的 client 构造 Tracing,说明该 API 面向多种底层协议保持一致的设计。
最小可运行示例
将前面所有知识点串起来,一个完整的追踪流程如下(源自 Tracing 类文档的 Example):
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); // 1. 启动追踪:写入 trace.json,并记录截图帧 await page.tracing.start({path: 'trace.json', screenshots: true}); // 2. 驱动页面执行需要审计的动作 await page.goto('https://example.com'); await page.waitForSelector('img'); // 3. 停止追踪 await page.tracing.stop(); await browser.close();运行后当前目录会生成trace.json,你可以直接把它拖入 Chrome DevTools 的 Performance 面板,或交给 Timeline Viewer 等工具进行时间线回放与分析。
进阶用法与常见问题
1. 不落盘,直接拿内存数据
如果不需要保存文件,只想在进程内快速分析,可省略path:
await page.tracing.start(); await page.goto('https://example.com'); const trace = await page.tracing.stop(); // Uint8Array | undefined if (trace) { const text = new TextDecoder().decode(trace); const parsed = JSON.parse(text); // 访问 parsed.traceEvents }这正是 Tracing.stop() 返回值Promise<Uint8Array | undefined>的用途——没有指定path时数据以Uint8Array返回。测试用例 验证了无 path 场景下仍能拿到非空的 trace 字节流。需要注意,由于 trace 以 JSON 形式组织,字节长度通常远大于 10 字节,上面的测试仅用于确认数据非空。
同时,即使指定了path,stop()的返回值也会与写入文件的内容保持一致(测试断言Buffer.from(trace).toString()与文件内容完全相等,见 测试用例),所以两套消费方式可以按需混用。
2. 完全自定义类别
当你只需要某个非常狭窄的事件子集时,可以用-*先关掉全部,再显式放行所需类别:
await page.tracing.start({ path: 'narrow-trace.json', categories: ['-*', 'disabled-by-default-devtools.timeline.frame'], });3. 追踪开始与结束之间做什么
追踪是异步启停的,start()与stop()之间执行的任何goto、click、waitForSelector等操作都会被记录在时间线中,这正是性能审计的标准姿势:先开录、再执行目标场景、最后收尾停止。
4. 常见失败场景
- 重复启动:前一次追踪未
stop()就再次start(),抛出断言错误(见前文"只允许一个活动追踪")。 - 跨页面启动:同一浏览器下两个页面同时开启追踪,第二个调用失败,需先关闭第一个。
- 路径不可写:如果
path指向无法写入的位置(例如指向一个目录),stop()在读流/写盘阶段会因底层流错误而 reject。测试中用path: import.meta.dirname(一个目录路径)复现了该异常场景,最终stop()抛错(见 测试用例)。
总结
page.tracing.start()是 Puppeteer 性能追踪流程的入口,配合stop()即可在自动化会话中产出可被 Chrome DevTools 直接分析的 trace 文件。围绕 start 文档 展开的四个配置项——path、screenshots、categories与bufferSize——分别控制数据去向、截图帧、事件类别范围与缓冲区大小;理解这些选项后,你可以按需裁剪 trace 体积或扩展录制窗口。而在这些易用 API 的背后,是Tracing类通过Tracing.start/Tracing.endCDP 命令配合ReturnAsStream传输模式完成的高效数据链路,这套机制让大体积 trace 也能稳定、完整地送达你的分析工具。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考