一句话总结:CDP 不仅能控制浏览器,还能深入 Chrome 的性能分析引擎,拿到 LCP、FCP、CLS 等 Core Web Vitals 指标,甚至可以集成 Lighthouse 做全自动化的性能审计。
目录
- 为什么用 CDP 做性能分析
- Performance 域:快速获取基础指标
- 实战一:采集 Core Web Vitals
- 实战二:性能追踪 Tracing
- 实战三:Lighthouse 自动化审计
- 实战四:性能监控告警系统
- 实战五:性能回归测试 CI 集成
- 踩坑记录与最佳实践
为什么用 CDP 做性能分析
传统前端性能采集方式各有局限:
| 方案 |
优点 |
缺点 |
Performance API (window.performance) |
浏览器原生、简单 |
只能拿到 JS 层数据,无法采集 Tracing 信息 |
| Web Vitals 库 (web-vitals) |
标准化指标 |
依赖用户访问、无法做自动化测试 |
| Lighthouse CLI |
报告全面 |
只能独立运行、难以集成到自动化流程 |
| Chrome DevTools 手动操作 |
可视化友好 |
不能批量、不能自动化 |
| CDP Performance / Tracing 域 |
引擎级数据、可编程 |
需要理解 CDP 协议 |
CDP 方案的优势:
- 引擎级数据:直接采集 Chrome 性能追踪引擎的原始数据
- 可编程:完全自动化,适合 CI/CD
- 无侵入:不需要在页面中注入 JavaScript
- 指标全面:从 LCP 到 Tracing 事件,覆盖所有层面
Performance 域提供了最简单的性能数据获取方式。它对应的是 DevTools Performance 面板的数据。
启用和采集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| CMD_ID = [0] async def cdp(ws, method, params=None): """发送 CDP 命令并等待返回结果""" CMD_ID[0] += 1 cmd_id = CMD_ID[0] request = {'id': cmd_id, 'method': method, 'params': params or {}} await ws.send(json.dumps(request)) async for msg in ws: response = json.loads(msg) if response.get('id') == cmd_id: return response.get('result', {})
async def collect_performance_metrics(ws): """采集页面性能指标""" await cdp(ws, 'Performance.enable') await cdp(ws, 'Page.navigate', {'url': 'https://example.com'}) await asyncio.sleep(5) result = await cdp(ws, 'Performance.getMetrics') metrics = result.get('metrics', []) data = {} for m in metrics: data[m['name']] = m['value'] return data
metrics = await collect_performance_metrics(ws) for name, value in sorted(metrics.items()): print(f'{name}: {value}')
|
关键指标说明
Performance.getMetrics 返回的指标包括:
| 指标名称 |
含义 |
说明 |
Timestamp |
时间戳 |
采集时刻的时间 |
Documents |
DOM 数量 |
文档对象数量 |
Frames |
帧数 |
已渲染的帧数 |
JSEventListeners |
JS 事件监听器 |
注册的事件监听器数量 |
Nodes |
DOM 节点数 |
页面中的 DOM 元素数量 |
LayoutCount |
布局次数 |
触发的回流次数 |
RecalcStyleCount |
样式重算次数 |
样式重新计算的次数 |
LayoutDuration |
布局耗时 |
所有布局耗时总和 |
RecalcStyleDuration |
样式重算耗时 |
所有样式重算耗时总和 |
ScriptDuration |
脚本执行耗时 |
JavaScript 执行总耗时 |
TaskDuration |
任务耗时 |
所有任务耗时总和 |
JSHeapUsedSize |
JS 堆使用量 |
JavaScript 已用堆内存 |
JSHeapTotalSize |
JS 堆总量 |
JavaScript 总堆内存 |
注意:这些是指页面加载整个过程的汇总数据,而不是每个事件的细粒度时间点。
实战一:采集 Core Web Vitals
Core Web Vitals 是 Google 定义的三个核心用户体验指标:LCP、FID/INP、CLS。CDP 没有直接的 API 返回这些指标,但可以通过拦截 PerformanceEntry 来采集。
方案一:通过 Runtime.evaluate 读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| async def collect_web_vitals_js(ws): """通过 JS 采集 Web Vitals""" result = await cdp(ws, 'Runtime.evaluate', { 'expression': ''' (() => { const entries = performance.getEntriesByType('paint'); const result = {}; entries.forEach(e => { result[e.name] = e.startTime; }); // LCP: 从 PerformanceObserver 获取 // 但注意:LCP 可能需要页面完全加载后才稳定 const nav = performance.getEntriesByType('navigation')[0]; if (nav) { result['TTFB'] = nav.responseStart - nav.requestStart; result['DOMContentLoaded'] = nav.domContentLoadedEventEnd; result['Load'] = nav.loadEventEnd; result['DomInteractive'] = nav.domInteractive; } return JSON.stringify(result); })() ''', 'returnByValue': True }) return json.loads(result['result']['value'])
|
对于 LCP(Largest Contentful Paint)和 CLS(Cumulative Layout Shift),需要用 PerformanceObserver 来监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| async def capture_lcp_and_cls(ws, timeout=10): """通过 PerformanceObserver 捕获 LCP 和 CLS""" await cdp(ws, 'Runtime.evaluate', { 'expression': ''' window.__webVitals = {}; // 监听 LCP new PerformanceObserver((list) => { const entries = list.getEntries(); if (entries.length > 0) { window.__webVitals['LCP'] = entries[entries.length - 1].startTime; window.__webVitals['LCP_Element'] = entries[entries.length - 1].element?.tagName || ''; } }).observe({type: 'largest-contentful-paint', buffered: true}); // 监听 CLS let clsValue = 0; new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } window.__webVitals['CLS'] = clsValue; }).observe({type: 'layout-shift', buffered: true}); // 监听 FID (First Input Delay) new PerformanceObserver((list) => { for (const entry of list.getEntries()) { window.__webVitals['FID'] = entry.processingStart - entry.startTime; break; } }).observe({type: 'first-input', buffered: true}); ''' }) await cdp(ws, 'Page.navigate', {'url': 'https://example.com'}) await asyncio.sleep(timeout) result = await cdp(ws, 'Runtime.evaluate', { 'expression': 'JSON.stringify(window.__webVitals)', 'returnByValue': True }) return json.loads(result['result']['value'])
|
判断性能等级
Google 建议的性能阈值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| def grade_web_vitals(vitals): """根据 Web Vitals 值给出评级""" grades = {} lcp = vitals.get('LCP', 0) if lcp <= 2500: grades['LCP'] = ('✅ 良好', lcp) elif lcp <= 4000: grades['LCP'] = ('⚠️ 需改进', lcp) else: grades['LCP'] = ('❌ 较差', lcp) cls = vitals.get('CLS', 0) if cls <= 0.1: grades['CLS'] = ('✅ 良好', cls) elif cls <= 0.25: grades['CLS'] = ('⚠️ 需改进', cls) else: grades['CLS'] = ('❌ 较差', cls) ttfb = vitals.get('TTFB', 0) if ttfb <= 800: grades['TTFB'] = ('✅ 良好', ttfb) elif ttfb <= 1800: grades['TTFB'] = ('⚠️ 需改进', ttfb) else: grades['TTFB'] = ('❌ 较差', ttfb) fid = vitals.get('FID', 0) if fid <= 100: grades['FID'] = ('✅ 良好', fid) elif fid <= 300: grades['FID'] = ('⚠️ 需改进', fid) else: grades['FID'] = ('❌ 较差', fid) return grades
vitals = capture_lcp_and_cls(ws) grades = grade_web_vitals(vitals) for metric, (grade, value) in grades.items(): print(f'{metric}: {grade} ({value:.1f})')
|
实战二:性能追踪 Tracing
Tracing 是 CDP 最强大的性能分析功能。它采集 Chrome 引擎层面的完整追踪数据,包括 JS 执行、渲染、布局、绘制、GPU 等所有信息。
启动和停止 Tracing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| async def trace_page(ws, url, categories=None, timeout=10): """ 对页面进行性能追踪 Args: ws: CDP WebSocket 连接 url: 目标 URL categories: 追踪类别,默认使用常用类别 timeout: 采集时长 Returns: 追踪事件列表 """ if categories is None: categories = [ 'devtools.timeline', 'disabled-by-default-devtools.timeline', 'disabled-by-default-devtools.timeline.frame', 'disabled-by-default-devtools.timeline.stack', 'disabled-by-default-v8.cpu_profile', 'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.compile', 'toplevel', 'blink.console', 'blink.user_timing', 'latencyInfo', 'loading', 'navigation', ] await cdp(ws, 'Tracing.start', { 'categories': ','.join(categories), 'options': 'sampling-frequency=10000', }) await cdp(ws, 'Page.navigate', {'url': url}) await asyncio.sleep(timeout) await cdp(ws, 'Tracing.end') events = [] try: async with asyncio.timeout(5): async for msg in ws: data = json.loads(msg) method = data.get('method', '') if method == 'Tracing.tracingComplete': break if method == 'Tracing.dataCollected': collected = data['params'].get('value', []) events.extend(collected) except (asyncio.TimeoutError, Exception): pass return events
|
分析追踪事件
Tracing 采集的数据量通常很大(几万到几十万条事件)。关键的事件类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| def analyze_trace_events(events): """分析追踪事件,提取关键性能数据""" analysis = { 'script_compile': [], 'script_evaluate': [], 'layout': [], 'paint': [], 'parse_html': [], 'resource_loading': [], 'long_tasks': [], } for event in events: name = event.get('name', '') cat = event.get('cat', '') dur = event.get('dur', 0) / 1000 args = event.get('args', {}) if name == 'v8.compile' or 'V8.Compile' in name: analysis['script_compile'].append({ 'duration_ms': dur, 'url': args.get('data', {}).get('url', 'unknown') }) if name == 'Layout': analysis['layout'].append({ 'duration_ms': dur, 'dirty_objects': args.get('dirtyObjects', 0), 'partial_layout': args.get('partialLayout', False) }) if name == 'Paint': analysis['paint'].append({ 'duration_ms': dur }) if dur > 50: analysis['long_tasks'].append({ 'name': name, 'duration_ms': dur, 'cat': cat }) return analysis
def print_analysis(analysis): """打印分析报告""" print('=== 性能追踪分析报告 ===') print() compile_time = sum(t['duration_ms'] for t in analysis['script_compile']) print(f'📜 JS 编译总耗时: {compile_time:.1f}ms') for t in sorted(analysis['script_compile'], key=lambda x: -x['duration_ms'])[:5]: print(f' - {t["url"][:60]}: {t["duration_ms"]:.1f}ms') layout_count = len(analysis['layout']) layout_time = sum(t['duration_ms'] for t in analysis['layout']) print(f'\n📐 布局次数: {layout_count}, 总耗时: {layout_time:.1f}ms') paint_count = len(analysis['paint']) paint_time = sum(t['duration_ms'] for t in analysis['paint']) print(f'🎨 绘制次数: {paint_count}, 总耗时: {paint_time:.1f}ms') long_tasks = analysis['long_tasks'] print(f'\n⚠️ 长任务(>50ms): {len(long_tasks)} 个') for t in sorted(long_tasks, key=lambda x: -x['duration_ms'])[:10]: print(f' - {t["name"]}: {t["duration_ms"]:.1f}ms')
|
从 Tracing 数据计算 LCP
Tracing 数据中包含了 LCP 事件,可以从 largestContentfulPaint::Candidate 事件中准确获取 LCP 时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def extract_lcp_from_trace(events): """从 Tracing 事件中提取精确的 LCP 时间""" lcp_events = [] for event in events: name = event.get('name', '') if 'largestContentfulPaint' in name or 'LCP' in name: lcp_events.append({ 'time': event.get('ts', 0) / 1000, 'dur': event.get('dur', 0) / 1000, 'args': event.get('args', {}) }) return lcp_events
|
实战三:Lighthouse 自动化审计
Lighthouse 是 Google 官方出品的网站质量审计工具。虽然它通常作为独立 CLI 运行,但也可以通过 CDP 集成到自动化流程中。
重要说明:CDP 协议中没有 Lighthouse 域(domain),因此无法直接通过 CDP 命令调用 Lighthouse 审计。Lighthouse.start 并非 CDP 标准协议方法。
以下两种是经过验证的正确方案:
方案一:通过命令行调用 Lighthouse + CDP 端口
更通用的方式是用 Node.js Lighthouse CLI 配合 CDP 端口:
1 2 3 4 5 6 7 8 9 10
| npm install -g lighthouse
lighthouse https://example.com \ --chrome-flags="--remote-debugging-port=9222" \ --output=json \ --output-path=./lighthouse-report.json \ --preset=desktop \ --quiet
|
但既然我们用的是 Python,可以直接用 subprocess 调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import subprocess, json
def run_lighthouse(url, output_path='lighthouse-report.json', port=9222): """运行 Lighthouse 审计""" cmd = [ 'npx', 'lighthouse', url, '--chrome-flags', f'--remote-debugging-port={port}', '--output', 'json', '--output-path', output_path, '--preset', 'desktop', '--quiet' ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode == 0: with open(output_path, 'r') as f: report = json.load(f) categories = report.get('categories', {}) audits = report.get('audits', {}) return { 'scores': { name: data['score'] * 100 for name, data in categories.items() }, 'metrics': { 'lcp': audits.get('largest-contentful-paint', {}).get('numericValue', 0), 'fcp': audits.get('first-contentful-paint', {}).get('numericValue', 0), 'cls': audits.get('cumulative-layout-shift', {}).get('numericValue', 0), 'tbt': audits.get('total-blocking-time', {}).get('numericValue', 0), 'si': audits.get('speed-index', {}).get('numericValue', 0), 'ttfb': audits.get('server-response-time', {}).get('numericValue', 0), } } else: return {'error': result.stderr}
report = run_lighthouse('https://example.com') if 'scores' in report: for category, score in report['scores'].items(): print(f'{category}: {score:.0f}/100') print(f'LCP: {report["metrics"]["lcp"]:.0f}ms') print(f'CLS: {report["metrics"]["cls"]:.3f}')
|
方案二:纯 Python Lighthouse 解析
如果不想依赖 Node.js,也可以完全用 CDP 数据自行计算类似 Lighthouse 的指标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def compute_performance_score(metrics): """根据 CDP 采集的指标计算类 Lighthouse 分数""" scores = {} fcp = metrics.get('first-contentful-paint', 3000) if fcp <= 1800: scores['fcp'] = 100 - (fcp / 1800) * 30 elif fcp <= 3000: scores['fcp'] = 70 - ((fcp - 1800) / 1200) * 40 else: scores['fcp'] = max(0, 30 - ((fcp - 3000) / 1000) * 30) scores['overall'] = sum(scores.values()) / len(scores) if scores else 0 return scores
|
实战四:性能监控告警系统
将以上技巧整合成一个定时监控系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| import asyncio, json, urllib.request, websockets, os from datetime import datetime
class CDPPerformanceMonitor: """CDP 性能监控器""" THRESHOLDS = { 'LCP': 2500, 'FCP': 1800, 'CLS': 0.1, 'TTFB': 800, 'JSHeapUsedSize': 50000000, } def __init__(self, host='localhost:9222', log_dir='./perf_logs'): self.host = host self.log_dir = log_dir self.ws = None os.makedirs(log_dir, exist_ok=True) async def _connect(self): data = json.loads( urllib.request.urlopen(f'http://{self.host}/json', timeout=5).read() ) ws_url = data[0]['webSocketDebuggerUrl'] self.ws = await websockets.connect(ws_url, max_size=2**24) await self.cdp('Page.enable') await self.cdp('Performance.enable') async def cdp(self, method, params=None): return await cdp(self.ws, method, params) async def check_url(self, url, label=''): """检查单个 URL 的性能""" await self._connect() print(f'🔍 Checking {label or url}...') await self.cdp('Page.navigate', {'url': url}) await asyncio.sleep(5) result = await self.cdp('Performance.getMetrics') metrics = {m['name']: m['value'] for m in result.get('metrics', [])} vitals_result = await self.cdp('Runtime.evaluate', { 'expression': ''' (() => { const nav = performance.getEntriesByType('navigation')[0]; const paint = performance.getEntriesByType('paint'); const fcp = paint.find(e => e.name === 'first-contentful-paint'); return JSON.stringify({ TTFB: nav ? nav.responseStart - nav.requestStart : 0, FCP: fcp ? fcp.startTime : 0, DomContentLoaded: nav ? nav.domContentLoadedEventEnd : 0 }); })() ''', 'returnByValue': True }) vitals = json.loads(vitals_result['result']['value']) report = { 'url': url, 'label': label, 'timestamp': datetime.now().isoformat(), 'metrics': {**metrics, **vitals} } alerts = [] for metric, threshold in self.THRESHOLDS.items(): value = report['metrics'].get(metric, 0) if value > threshold: alerts.append(f'⚠️ {metric}: {value:.1f} (阈值: {threshold})') if alerts: print(' ALERTS:') for alert in alerts: print(f' {alert}') else: print(' ✅ All metrics within thresholds') log_file = os.path.join( self.log_dir, f'{label or url.replace("://", "_").replace("/", "_")}.json' ) with open(log_file, 'w') as f: json.dump(report, f, indent=2) await self.ws.close() return report, alerts async def check_multiple(self, urls): """批量检查多个 URL""" all_reports = [] all_alerts = {} for url, label in urls: try: report, alerts = await self.check_url(url, label) all_reports.append(report) if alerts: all_alerts[label or url] = alerts except Exception as e: print(f'❌ Error checking {url}: {e}') print(f'\n{"="*40}') print(f'📊 检查完成: {len(all_reports)}/{len(urls)} 成功') if all_alerts: print(f'⚠️ {len(all_alerts)} 个页面触发告警:') for page, alerts in all_alerts.items(): for alert in alerts: print(f' {page}: {alert}') else: print('✅ 所有页面正常') return all_reports
async def demo(): monitor = CDPPerformanceMonitor() await monitor.check_multiple([ ('https://cdp.autify.cc', '首页'), ('https://cdp.autify.cc/cdp-python-automation-guide/', 'CDP 完全指南'), ('https://cdp.autify.cc/cdp-network-intercept-guide/', '网络拦截篇'), ])
asyncio.run(demo())
|
实战五:性能回归测试 CI 集成
在 CI/CD 中集成性能检查,防止性能退化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| async def performance_regression_check(url, baseline_file='baseline.json'): """ 性能回归测试:对比当前结果与基线 Args: url: 要测试的 URL baseline_file: 基线数据文件 Returns: (passed, changes): 是否通过和变更详情 """ baseline = {} if os.path.exists(baseline_file): with open(baseline_file, 'r') as f: baseline = json.load(f) monitor = CDPPerformanceMonitor() report, _ = await monitor.check_url(url) changes = {} passed = True KEY_METRICS = { 'ScriptDuration': 0.2, 'LayoutCount': 0.2, 'JSHeapUsedSize': 0.15, } for metric, tolerance in KEY_METRICS.items(): current = report['metrics'].get(metric, 0) base_val = baseline.get(metric, current) if base_val > 0: change = (current - base_val) / base_val if change > tolerance: changes[metric] = { 'baseline': base_val, 'current': current, 'change_pct': round(change * 100, 1), 'status': 'FAIL' } passed = False elif change < -tolerance: changes[metric] = { 'baseline': base_val, 'current': current, 'change_pct': round(change * 100, 1), 'status': 'IMPROVED' } else: changes[metric] = { 'baseline': base_val, 'current': current, 'change_pct': round(change * 100, 1), 'status': 'OK' } with open(baseline_file, 'w') as f: json.dump(report['metrics'], f, indent=2) return passed, changes
passed, changes = await performance_regression_check('https://cdp.autify.cc/') if not passed: print('❌ 性能回归测试未通过') for metric, info in changes.items(): if info['status'] == 'FAIL': print(f' {metric}: {info["change_pct"]}% 退化') exit(1) else: print('✅ 性能回归测试通过')
|
踩坑记录与最佳实践
1. Tracing 数据量巨大
一次 10 秒的 Tracing 可能产生 10 万+ 条事件,内存占用很大。建议:
1 2 3 4 5 6 7 8 9
| TRACING_TIMEOUT = 5
await cdp(ws, 'Tracing.start', { 'categories': 'devtools.timeline', })
|
Performance.getMetrics 返回的是调用时刻的累计值,不是页面加载完成时的值。要在页面完全加载后再获取:
1 2 3 4 5 6 7 8
| await cdp(ws, 'Page.navigate', {'url': url}) metrics = await cdp(ws, 'Performance.getMetrics')
await cdp(ws, 'Page.navigate', {'url': url}) await wait_for_page_loaded(ws) metrics = await cdp(ws, 'Performance.getMetrics')
|
判断页面加载完成的方法:
1 2 3 4 5 6 7 8 9 10 11
| async def wait_for_page_loaded(ws, timeout=15): """等待页面 load 事件""" try: async with asyncio.timeout(timeout): async for msg in ws: data = json.loads(msg) if data.get('method') == 'Page.loadEventFired': return True except (asyncio.TimeoutError, Exception): pass return False
|
3. Lighthouse 版本兼容性
不同 Chrome 版本内置的 Lighthouse 版本不同,生成的报告格式可能有差异。建议锁定 Chrome 版本,或者在 CI 中显式指定 Lighthouse CLI 版本。
4. 网络条件模拟
测试性能时,需要控制网络条件以保证结果可重复:
1 2 3 4 5 6 7 8
| await cdp(ws, 'Network.emulateNetworkConditions', { 'offline': False, 'latency': 150, 'downloadThroughput': 750 * 1024 / 8, 'uploadThroughput': 250 * 1024 / 8, 'connectionType': 'cellular3g' })
|
5. 多次取平均值
单次性能测试的波动很大(受 CPU、内存等影响),建议多次测试取中位数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async def median_performance(url, n=5): """运行 n 次性能测试,取中位数""" results = [] for i in range(n): print(f' 第 {i+1}/{n} 次...') monitor = CDPPerformanceMonitor() report, _ = await monitor.check_url(url) results.append(report['metrics'].get('ScriptDuration', 0)) results.sort() median = results[len(results) // 2] print(f' ScriptDuration 中位数: {median:.1f}ms ({n} 次)') return median
|
总结
CDP 的性能分析能力覆盖了从简单指标采集到深度 Tracing 的完整链条:
- Performance 域:快速获取基础指标(DOM 数量、JS 堆、布局次数等)
- Web Vitals 采集:通过 PerformanceObserver 捕获 LCP / CLS / FID
- Tracing 域:采集引擎级追踪数据,分析长任务、布局抖动等问题
- Lighthouse 集成:获取完整的性能评分和优化建议
- 监控告警:将性能检查集成到 CI/CD 流水线中
性能分析方案选择:
| 需求 |
推荐方案 |
| 快速了解页面健康度 |
Performance.getMetrics + Web Vitals |
| 定位性能瓶颈 |
Tracing 分析 |
| 生成优化报告 |
Lighthouse |
| 持续监控 |
定时监控 + 回归测试 |
| CI/CD 质量门禁 |
回归测试 + 阈值告警 |
上一篇回顾:CDP 浏览器指纹与反检测实战:用 Python 修改指纹绕过自动化检测。
下一篇预告:CDP 操作 Cookie 完全指南:用 Python 实现增删改查与自动化登录。