如何在Playwright测试中通过new_cdp_session测量Total Blocking Time?
解决Playwright中获取Web Vitals(TBT、LCP、CLS)的问题
你遇到的问题核心在于:TBT、LCP、CLS属于Web Vitals指标,它们并非Chrome DevTools Protocol(CDP)Performance.getMetrics接口直接返回的原生指标,而是基于浏览器底层性能数据计算得出的衍生值。下面提供两种可行的解决方法:
方法一:通过CDP的PerformanceObserver监听指标
利用CDP的性能事件监听能力,直接捕获Web Vitals相关的性能条目,再进行计算:
from playwright.sync_api import Page def test_web_vitals_via_cdp(page: Page): client = page.context.new_cdp_session(page) # 启用性能监控,设置时间域为时间戳 client.send('Performance.enable') client.send('Performance.setTimeDomain', {'timeDomain': 'timeStamp'}) # 存储指标结果 lcp_value = None cls_value = 0.0 # 监听LCP事件 def handle_lcp(event): nonlocal lcp_value entry = event['params']['entry'] if entry['name'] == 'largest-contentful-paint': lcp_value = entry['startTime'] + entry['duration'] client.on('Performance.entryAdded', handle_lcp) # 监听CLS事件(忽略用户输入后的位移) def handle_layout_shift(event): nonlocal cls_value entry = event['params']['entry'] if entry['name'] == 'layout-shift' and not entry.get('hadRecentInput', False): cls_value += entry['value'] client.on('Performance.entryAdded', handle_layout_shift) # 导航到页面并等待加载稳定 page.goto('https://www.google.com', wait_until='networkidle') # 计算TBT:统计所有长任务中超过50ms的阻塞部分 def calculate_tbt(): long_tasks = client.send('Performance.getEntries', {'type': 'longtask'}) total_blocking_time = 0.0 for task in long_tasks: blocking_part = task['duration'] - 50 if blocking_part > 0: total_blocking_time += blocking_part return total_blocking_time tbt_value = calculate_tbt() # 输出结果 print(f"LCP: {lcp_value:.2f}ms") print(f"CLS: {cls_value:.4f}") print(f"TBT: {tbt_value:.2f}ms") client.send('Performance.disable')
方法二:直接使用Playwright的页面evaluate方法(更简洁)
无需手动操作CDP,直接通过page.evaluate调用浏览器原生Performance API获取并计算指标:
from playwright.sync_api import Page def test_web_vitals_simpler(page: Page): page.goto('https://www.google.com', wait_until='networkidle') # 获取LCP lcp = page.evaluate("""() => { return new Promise(resolve => { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; resolve(lastEntry.startTime + lastEntry.duration); }); observer.observe({ type: 'largest-contentful-paint', buffered: true }); }); }""") # 获取CLS cls = page.evaluate("""() => { return new Promise(resolve => { let totalCls = 0; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) totalCls += entry.value; } }); observer.observe({ type: 'layout-shift', buffered: true }); // 等待页面稳定后返回结果 requestIdleCallback(() => resolve(totalCls)); }); }""") # 获取TBT tbt = page.evaluate("""() => { const longTasks = performance.getEntriesByType('longtask'); return longTasks.reduce((total, task) => { const blocking = task.duration - 50; return total + (blocking > 0 ? blocking : 0); }, 0); }""") print(f"LCP: {lcp:.2f}ms") print(f"CLS: {cls:.4f}") print(f"TBT: {tbt:.2f}ms")
关键说明
- LCP:取最后一个
largest-contentful-paint条目的开始时间与时长之和,代表页面最大内容渲染完成的时间。 - CLS:累加所有无用户输入触发的
layout-shift条目值,反映页面布局的稳定性。 - TBT:统计所有长任务(时长>50ms)中超出50ms的部分,这些是导致页面卡顿的阻塞时间。
内容的提问来源于stack exchange,提问作者Toki D
相关产品推荐
相关产品推荐

