Puppeteer 如何使用 Chrome DevTools Protocol (CDP) 进行高级调试和性能分析?
Puppeteer 提供了丰富的 Chrome DevTools Protocol (CDP) 功能,允许开发者访问浏览器底层的调试和性能分析能力。1. CDP 基础创建 CDP 会话:const client = await page.target().createCDPSession();启用 CDP 域:await client.send('Performance.enable');await client.send('Network.enable');await client.send('Runtime.enable');发送 CDP 命令:const result = await client.send('Performance.getMetrics');console.log(result);监听 CDP 事件:client.on('Network.requestWillBeSent', (params) => { console.log('Request:', params.request.url);});2. 性能监控启用性能监控:const client = await page.target().createCDPSession();await client.send('Performance.enable');获取性能指标:const metrics = await client.send('Performance.getMetrics');console.log('Performance Metrics:', metrics.metrics);关键性能指标:const 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});性能追踪:// 开始追踪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. 网络监控启用网络监控:const client = await page.target().createCDPSession();await client.send('Network.enable');监控网络请求:client.on('Network.requestWillBeSent', (params) => { console.log('Request:', { url: params.request.url, method: params.request.method, type: params.type });});监控网络响应:client.on('Network.responseReceived', (params) => { console.log('Response:', { url: params.response.url, status: params.response.status, mimeType: params.response.mimeType });});获取请求体:client.on('Network.requestWillBeSent', async (params) => { if (params.request.postData) { console.log('Request body:', params.request.postData); }});获取响应体:client.on('Network.responseReceived', async (params) => { const responseBody = await client.send('Network.getResponseBody', { requestId: params.requestId }); console.log('Response body:', responseBody.body);});4. 运行时调试启用运行时监控:const client = await page.target().createCDPSession();await client.send('Runtime.enable');执行 JavaScript:const result = await client.send('Runtime.evaluate', { expression: 'document.title'});console.log('Result:', result.result.value);获取控制台日志:client.on('Runtime.consoleAPICalled', (params) => { console.log('Console:', params.type, params.args);});监听异常:client.on('Runtime.exceptionThrown', (params) => { console.error('Exception:', params.exceptionDetails);});5. DOM 监控启用 DOM 监控:const client = await page.target().createCDPSession();await client.send('DOM.enable');获取文档根节点:const root = await client.send('DOM.getDocument');console.log('Root node:', root.root);查询节点:const result = await client.send('DOM.querySelector', { nodeId: root.root.nodeId, selector: '.my-element'});console.log('Node:', result.nodeId);获取节点属性:const attributes = await client.send('DOM.getAttributes', { nodeId: result.nodeId});console.log('Attributes:', attributes.attributes);6. Page 监控启用 Page 监控:const client = await page.target().createCDPSession();await client.send('Page.enable');监听页面加载:client.on('Page.loadEventFired', () => { console.log('Page loaded');});监听导航:client.on('Page.frameNavigated', (params) => { console.log('Navigated to:', params.frame.url);});获取页面资源树:const resourceTree = await client.send('Page.getResourceTree');console.log('Resource tree:', resourceTree);7. 实际应用场景场景 1:性能分析工具async 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:网络请求分析async 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:内存分析async 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 执行分析async 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 高级功能覆盖代码:const 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);监控长任务:const 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'); } });});监控布局抖动:const 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 域:try { await client.send('Performance.enable'); // 操作} finally { await client.send('Performance.disable');}2. 批量获取数据:// 一次性获取多个指标const [metrics, networkData] = await Promise.all([ client.send('Performance.getMetrics'), client.send('Network.getResponseBody', { requestId: 'xxx' })]);3. 使用事件过滤:client.on('Network.requestWillBeSent', (params) => { // 只处理特定请求 if (params.request.url.includes('/api/')) { console.log('API Request:', params.request.url); }});4. 错误处理:try { await client.send('Performance.getMetrics');} catch (error) { console.error('CDP error:', error); // 降级处理}