Puppeteer 提供了丰富的 Chrome DevTools Protocol (CDP) 功能,允许开发者访问浏览器底层的调试和性能分析能力。
1. CDP 基础
创建 CDP 会话:
javascriptconst client = await page.target().createCDPSession();
启用 CDP 域:
javascriptawait client.send('Performance.enable'); await client.send('Network.enable'); await client.send('Runtime.enable');
发送 CDP 命令:
javascriptconst result = await client.send('Performance.getMetrics'); console.log(result);
监听 CDP 事件:
javascriptclient.on('Network.requestWillBeSent', (params) => { console.log('Request:', params.request.url); });
2. 性能监控
启用性能监控:
javascriptconst client = await page.target().createCDPSession(); await client.send('Performance.enable');
获取性能指标:
javascriptconst metrics = await client.send('Performance.getMetrics'); console.log('Performance Metrics:', metrics.metrics);
关键性能指标:
javascriptconst metrics = await client.send('Performance.getMetrics'); const metricMap = {}; metrics.metrics.forEach(m => metricMap[m.name] = m.value); console.log({ Timestamp: metricMap.Timestamp, Documents: metricMap.Documents, Frames: metricMap.Frames, JSEventListeners: metricMap.JSEventListeners, Nodes: metricMap.Nodes, LayoutCount: metricMap.LayoutCount, RecalcStyleCount: metricMap.RecalcStyleCount, LayoutDuration: metricMap.LayoutDuration, RecalcStyleDuration: metricMap.RecalcStyleDuration, ScriptDuration: metricMap.ScriptDuration, TaskDuration: metricMap.TaskDuration });
性能追踪:
javascript// 开始追踪 await client.send('Performance.enable'); await client.send('Tracing.start', { traceConfig: { includedCategories: ['devtools.timeline', 'blink.user_timing'] } }); // 执行操作 await page.goto('https://example.com'); // 停止追踪 const traceData = await client.send('Tracing.stop');
3. 网络监控
启用网络监控:
javascriptconst client = await page.target().createCDPSession(); await client.send('Network.enable');
监控网络请求:
javascriptclient.on('Network.requestWillBeSent', (params) => { console.log('Request:', { url: params.request.url, method: params.request.method, type: params.type }); });
监控网络响应:
javascriptclient.on('Network.responseReceived', (params) => { console.log('Response:', { url: params.response.url, status: params.response.status, mimeType: params.response.mimeType }); });
获取请求体:
javascriptclient.on('Network.requestWillBeSent', async (params) => { if (params.request.postData) { console.log('Request body:', params.request.postData); } });
获取响应体:
javascriptclient.on('Network.responseReceived', async (params) => { const responseBody = await client.send('Network.getResponseBody', { requestId: params.requestId }); console.log('Response body:', responseBody.body); });
4. 运行时调试
启用运行时监控:
javascriptconst client = await page.target().createCDPSession(); await client.send('Runtime.enable');
执行 JavaScript:
javascriptconst result = await client.send('Runtime.evaluate', { expression: 'document.title' }); console.log('Result:', result.result.value);
获取控制台日志:
javascriptclient.on('Runtime.consoleAPICalled', (params) => { console.log('Console:', params.type, params.args); });
监听异常:
javascriptclient.on('Runtime.exceptionThrown', (params) => { console.error('Exception:', params.exceptionDetails); });
5. DOM 监控
启用 DOM 监控:
javascriptconst client = await page.target().createCDPSession(); await client.send('DOM.enable');
获取文档根节点:
javascriptconst root = await client.send('DOM.getDocument'); console.log('Root node:', root.root);
查询节点:
javascriptconst result = await client.send('DOM.querySelector', { nodeId: root.root.nodeId, selector: '.my-element' }); console.log('Node:', result.nodeId);
获取节点属性:
javascriptconst attributes = await client.send('DOM.getAttributes', { nodeId: result.nodeId }); console.log('Attributes:', attributes.attributes);
6. Page 监控
启用 Page 监控:
javascriptconst client = await page.target().createCDPSession(); await client.send('Page.enable');
监听页面加载:
javascriptclient.on('Page.loadEventFired', () => { console.log('Page loaded'); });
监听导航:
javascriptclient.on('Page.frameNavigated', (params) => { console.log('Navigated to:', params.frame.url); });
获取页面资源树:
javascriptconst resourceTree = await client.send('Page.getResourceTree'); console.log('Resource tree:', resourceTree);
7. 实际应用场景
场景 1:性能分析工具
javascriptasync function analyzePerformance(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); const client = await page.target().createCDPSession(); // 启用性能监控 await client.send('Performance.enable'); await client.send('Network.enable'); const startTime = Date.now(); await page.goto(url, { waitUntil: 'networkidle2' }); const loadTime = Date.now() - startTime; // 获取性能指标 const metrics = await client.send('Performance.getMetrics'); const metricMap = {}; metrics.metrics.forEach(m => metricMap[m.name] = m.value); // 收集网络数据 const networkData = []; client.on('Network.requestWillBeSent', (params) => { networkData.push({ url: params.request.url, method: params.request.method, timestamp: params.timestamp }); }); const report = { url, loadTime, metrics: { layoutDuration: metricMap.LayoutDuration, recalcStyleDuration: metricMap.RecalcStyleDuration, scriptDuration: metricMap.ScriptDuration, taskDuration: metricMap.TaskDuration }, networkRequests: networkData.length }; await browser.close(); return report; } analyzePerformance('https://example.com').then(console.log);
场景 2:网络请求分析
javascriptasync function analyzeNetworkRequests(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); const client = await page.target().createCDPSession(); await client.send('Network.enable'); const requests = []; client.on('Network.requestWillBeSent', (params) => { requests.push({ requestId: params.requestId, url: params.request.url, method: params.request.method, type: params.type, timestamp: params.timestamp }); }); client.on('Network.responseReceived', (params) => { const request = requests.find(r => r.requestId === params.requestId); if (request) { request.status = params.response.status; request.mimeType = params.response.mimeType; request.size = params.response.encodedDataLength; } }); await page.goto(url, { waitUntil: 'networkidle2' }); // 分析请求 const analysis = { totalRequests: requests.length, byType: {}, byStatus: {}, totalSize: 0 }; requests.forEach(req => { // 按类型统计 if (!analysis.byType[req.type]) { analysis.byType[req.type] = { count: 0, size: 0 }; } analysis.byType[req.type].count++; analysis.byType[req.type].size += req.size || 0; // 按状态码统计 if (!analysis.byStatus[req.status]) { analysis.byStatus[req.status] = 0; } analysis.byStatus[req.status]++; analysis.totalSize += req.size || 0; }); await browser.close(); return analysis; } analyzeNetworkRequests('https://example.com').then(console.log);
场景 3:内存分析
javascriptasync function analyzeMemory(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); const client = await page.target().createCDPSession(); await client.send('Runtime.enable'); await client.send('HeapProfiler.enable'); await page.goto(url, { waitUntil: 'networkidle2' }); // 获取堆快照 const heapSnapshot = await client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false }); // 获取内存使用情况 const memoryMetrics = await client.send('Runtime.getHeapUsage'); const report = { totalSize: memoryMetrics.totalSize, usedSize: memoryMetrics.usedSize, heapSnapshot: heapSnapshot }; await browser.close(); return report; } analyzeMemory('https://example.com').then(console.log);
场景 4:JavaScript 执行分析
javascriptasync function analyzeJavaScript(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); const client = await page.target().createCDPSession(); await client.send('Runtime.enable'); await client.send('Debugger.enable'); const consoleLogs = []; const exceptions = []; client.on('Runtime.consoleAPICalled', (params) => { consoleLogs.push({ type: params.type, args: params.args.map(arg => arg.value) }); }); client.on('Runtime.exceptionThrown', (params) => { exceptions.push({ message: params.exceptionDetails.exception?.description, stackTrace: params.exceptionDetails.stackTrace }); }); await page.goto(url, { waitUntil: 'networkidle2' }); const report = { consoleLogs, exceptions, hasErrors: exceptions.length > 0 }; await browser.close(); return report; } analyzeJavaScript('https://example.com').then(console.log);
8. CDP 高级功能
覆盖代码:
javascriptconst client = await page.target().createCDPSession(); await client.send('DOM.enable'); await client.send('CSS.enable'); // 启用代码覆盖 await client.send('Profiler.enable'); await client.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }); // 执行操作 await page.goto('https://example.com'); // 获取覆盖数据 const coverage = await client.send('Profiler.takePreciseCoverage'); console.log('Coverage:', coverage.result);
监控长任务:
javascriptconst client = await page.target().createCDPSession(); await client.send('Performance.enable'); client.on('Performance.metrics', (params) => { params.metrics.forEach(metric => { if (metric.name === 'TaskDuration' && metric.value > 50) { console.warn('Long task detected:', metric.value, 'ms'); } }); });
监控布局抖动:
javascriptconst client = await page.target().createCDPSession(); await client.send('Performance.enable'); const layoutShifts = []; client.on('Performance.metrics', (params) => { params.metrics.forEach(metric => { if (metric.name === 'LayoutShift') { layoutShifts.push(metric.value); } }); }); // 计算累积布局偏移 const cls = layoutShifts.reduce((sum, shift) => sum + shift, 0); console.log('Cumulative Layout Shift:', cls);
9. 最佳实践
1. 及时禁用 CDP 域:
javascripttry { await client.send('Performance.enable'); // 操作 } finally { await client.send('Performance.disable'); }
2. 批量获取数据:
javascript// 一次性获取多个指标 const [metrics, networkData] = await Promise.all([ client.send('Performance.getMetrics'), client.send('Network.getResponseBody', { requestId: 'xxx' }) ]);
3. 使用事件过滤:
javascriptclient.on('Network.requestWillBeSent', (params) => { // 只处理特定请求 if (params.request.url.includes('/api/')) { console.log('API Request:', params.request.url); } });
4. 错误处理:
javascripttry { await client.send('Performance.getMetrics'); } catch (error) { console.error('CDP error:', error); // 降级处理 }