Skip to main content
performance-testing Web Vitals and load time testing patterns with Playwright. Use when measuring Core Web Vitals (LCP, FID, CLS), setting performance budgets, analyzing network performance, or profiling resource loading times.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/AbhiRKeesara/test-automation-skills --skill performance-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... name performance-testing description Web Vitals and load time testing patterns with Playwright. Use when measuring Core Web Vitals (LCP, FID, CLS), setting performance budgets, analyzing network performance, or profiling resource loading times.
Performance Testing Skill
Best practices for performance testing with Playwright to ensure your application meets performance requirements.
Why Performance Testing
User experience - Slow pages lead to user frustration and abandonment
SEO impact - Page speed affects search rankings
Business metrics - Performance correlates with conversion rates
Early detection - Catch performance regressions before production
Table of Contents
Web Vitals Measurement
Core Web Vitals
import { test, expect } from '@playwright/test' ;
test ('measure Core Web Vitals' , async ({ page }) => {
await page.addInitScript (() => {
. = {};
( {
entries = list. ();
lastEntry = entries[entries. - ];
. . = lastEntry. ;
}). ({ : , : });
( {
entries = list. ();
. . = entries[ ]. - entries[ ]. ;
}). ({ : , : });
clsValue = ;
( {
( entry list. ()) {
(!entry. ) {
clsValue += entry. ;
}
}
. . = clsValue;
}). ({ : , : });
});
page. ( );
page. ( );
page. ( );
metrics = page. ( . );
(metrics. ). ( );
(metrics. ). ( );
});
window
performanceMetrics
new
PerformanceObserver
(list ) =>
const
getEntries
const
length
1
window
performanceMetrics
lcp
startTime
observe
type
'largest-contentful-paint'
buffered
true
new
PerformanceObserver
(list ) =>
const
getEntries
window
performanceMetrics
fid
0
processingStart
0
startTime
observe
type
'first-input'
buffered
true
let
0
new
PerformanceObserver
(list ) =>
for
const
of
getEntries
if
hadRecentInput
value
window
performanceMetrics
cls
observe
type
'layout-shift'
buffered
true
await
goto
'/'
await
waitForLoadState
'networkidle'
await
waitForTimeout
1000
const
await
evaluate
() =>
window
performanceMetrics
expect
lcp
toBeLessThan
2500
expect
cls
toBeLessThan
0.1
Using Performance API test ('measure page load timing' , async ({ page }) => {
await page.goto ('/products' );
await page.waitForLoadState ('load' );
const timing = await page.evaluate (() => {
const perf = performance.getEntriesByType ('navigation' )[0 ] as PerformanceNavigationTiming ;
return {
dns : perf.domainLookupEnd - perf.domainLookupStart ,
tcp : perf.connectEnd - perf.connectStart ,
tls : perf.secureConnectionStart > 0 ? perf.connectEnd - perf.secureConnectionStart : 0 ,
ttfb : perf.responseStart - perf.requestStart ,
download : perf.responseEnd - perf.responseStart ,
domProcessing : perf.domComplete - perf.domInteractive ,
total : perf.loadEventEnd - perf.startTime ,
};
});
console .log ('Performance Timing:' , timing);
expect (timing.ttfb ).toBeLessThan (600 );
expect (timing.total ).toBeLessThan (3000 );
});
First Contentful Paint (FCP) test ('measure First Contentful Paint' , async ({ page }) => {
await page.goto ('/' );
const fcp = await page.evaluate (() => {
return new Promise <number >((resolve ) => {
new PerformanceObserver ((list ) => {
const entries = list.getEntries ();
const fcpEntry = entries.find (entry => entry.name === 'first-contentful-paint' );
if (fcpEntry) {
resolve (fcpEntry.startTime );
}
}).observe ({ type : 'paint' , buffered : true });
const existingEntry = performance.getEntriesByName ('first-contentful-paint' )[0 ];
if (existingEntry) {
resolve (existingEntry.startTime );
}
});
});
expect (fcp).toBeLessThan (1800 );
});
Network Performance
Request Timing test ('measure API response times' , async ({ page }) => {
const apiTimings : { url : string ; duration : number }[] = [];
page.on ('response' , async (response) => {
const timing = response.request ().timing ();
if (response.url ().includes ('/api/' )) {
apiTimings.push ({
url : response.url (),
duration : timing.responseEnd - timing.requestStart ,
});
}
});
await page.goto ('/dashboard' );
await page.waitForLoadState ('networkidle' );
console .log ('API Response Times:' , apiTimings);
for (const timing of apiTimings) {
expect (timing.duration ).toBeLessThan (1000 );
}
});
Network Throttling test ('performance under slow network' , async ({ page, context }) => {
const client = await context.newCDPSession (page);
await client.send ('Network.emulateNetworkConditions' , {
offline : false ,
downloadThroughput : (500 * 1024 ) / 8 ,
uploadThroughput : (500 * 1024 ) / 8 ,
latency : 400 ,
});
const startTime = Date .now ();
await page.goto ('/' );
await page.waitForLoadState ('domcontentloaded' );
const loadTime = Date .now () - startTime;
expect (loadTime).toBeLessThan (10000 );
await expect (page.getByRole ('heading' , { level : 1 })).toBeVisible ();
});
Resource Size Monitoring test ('monitor resource sizes' , async ({ page }) => {
const resources : { url : string ; size : number ; type : string }[] = [];
page.on ('response' , async (response) => {
const headers = response.headers ();
const contentLength = headers['content-length' ];
const contentType = headers['content-type' ] || 'unknown' ;
if (contentLength) {
resources.push ({
url : response.url (),
size : parseInt (contentLength),
type : contentType.split (';' )[0 ],
});
}
});
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
const byType = resources.reduce ((acc, r ) => {
const type = r.type ;
acc[type ] = (acc[type ] || 0 ) + r.size ;
return acc;
}, {} as Record <string , number >);
console .log ('Resources by type:' , byType);
const totalJS = byType['application/javascript' ] || 0 ;
const totalCSS = byType['text/css' ] || 0 ;
expect (totalJS).toBeLessThan (500 * 1024 );
expect (totalCSS).toBeLessThan (100 * 1024 );
});
Resource Loading
Image Loading Performance test ('image loading performance' , async ({ page }) => {
await page.goto ('/products' );
await page.waitForFunction (() => {
const images = Array .from (document .querySelectorAll ('img' ));
return images.every (img => img.complete && img.naturalHeight > 0 );
});
const imageMetrics = await page.evaluate (() => {
const images = performance.getEntriesByType ('resource' )
.filter ((r): r is PerformanceResourceTiming =>
r.initiatorType === 'img' || r.name .match (/\.(jpg|jpeg|png|webp|gif)$/i ) !== null
);
return images.map (img => ({
url : img.name ,
duration : img.responseEnd - img.startTime ,
size : img.transferSize ,
}));
});
console .log ('Image metrics:' , imageMetrics);
for (const img of imageMetrics) {
expect (img.duration ).toBeLessThan (2000 );
}
});
JavaScript Execution Time test ('measure JavaScript execution time' , async ({ page }) => {
const client = await page.context ().newCDPSession (page);
await client.send ('Profiler.enable' );
await client.send ('Profiler.start' );
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
const { profile } = await client.send ('Profiler.stop' );
const totalTime = profile.nodes .reduce ((acc, node ) => {
return acc + (node.hitCount || 0 ) * (profile.samplingInterval || 0 );
}, 0 );
console .log (`Total JS execution time: ${totalTime / 1000 } ms` );
expect (totalTime / 1000 ).toBeLessThan (1000 );
});
Performance Assertions
Custom Performance Assertions
import { Page , expect } from '@playwright/test' ;
interface PerformanceThresholds {
fcp ?: number ;
lcp ?: number ;
ttfb ?: number ;
totalLoad ?: number ;
}
export async function assertPerformance (
page : Page ,
thresholds : PerformanceThresholds
): Promise <void > {
const metrics = await page.evaluate (() => {
const navEntry = performance.getEntriesByType ('navigation' )[0 ] as PerformanceNavigationTiming ;
const paintEntries = performance.getEntriesByType ('paint' );
const fcpEntry = paintEntries.find (e => e.name === 'first-contentful-paint' );
return {
fcp : fcpEntry?.startTime || 0 ,
ttfb : navEntry.responseStart - navEntry.requestStart ,
totalLoad : navEntry.loadEventEnd - navEntry.startTime ,
};
});
if (thresholds.fcp ) {
expect (metrics.fcp , `FCP should be < ${thresholds.fcp} ms` ).toBeLessThan (thresholds.fcp );
}
if (thresholds.ttfb ) {
expect (metrics.ttfb , `TTFB should be < ${thresholds.ttfb} ms` ).toBeLessThan (thresholds.ttfb );
}
if (thresholds.totalLoad ) {
expect (metrics.totalLoad , `Total load should be < ${thresholds.totalLoad} ms` ).toBeLessThan (thresholds.totalLoad );
}
}
Using Performance Assertions import { assertPerformance } from '../utils/performance-assertions' ;
test ('homepage performance' , async ({ page }) => {
await page.goto ('/' );
await page.waitForLoadState ('load' );
await assertPerformance (page, {
fcp : 1500 ,
ttfb : 500 ,
totalLoad : 3000 ,
});
});
Performance Budgets
Define Performance Budgets
export const performanceBudgets = {
homepage : {
fcp : 1500 ,
lcp : 2500 ,
ttfb : 500 ,
totalLoad : 3000 ,
jsSize : 300 * 1024 ,
cssSize : 50 * 1024 ,
imageSize : 500 * 1024 ,
},
productList : {
fcp : 1800 ,
lcp : 2800 ,
ttfb : 600 ,
totalLoad : 4000 ,
jsSize : 400 * 1024 ,
cssSize : 60 * 1024 ,
imageSize : 800 * 1024 ,
},
checkout : {
fcp : 1200 ,
lcp : 2000 ,
ttfb : 400 ,
totalLoad : 2500 ,
jsSize : 350 * 1024 ,
cssSize : 50 * 1024 ,
imageSize : 200 * 1024 ,
},
};
Budget Enforcement Tests import { performanceBudgets } from '../performance-budgets' ;
test.describe ('Performance Budget Compliance' , () => {
test ('homepage stays within budget' , async ({ page }) => {
const budget = performanceBudgets.homepage ;
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
const timing = await page.evaluate (() => {
const nav = performance.getEntriesByType ('navigation' )[0 ] as PerformanceNavigationTiming ;
const fcp = performance.getEntriesByName ('first-contentful-paint' )[0 ];
return {
fcp : fcp?.startTime || 0 ,
ttfb : nav.responseStart - nav.requestStart ,
totalLoad : nav.loadEventEnd - nav.startTime ,
};
});
const resources = await page.evaluate (() => {
return performance.getEntriesByType ('resource' ).map ((r : PerformanceResourceTiming ) => ({
type : r.initiatorType ,
size : r.transferSize ,
}));
});
const jsSize = resources.filter (r => r.type === 'script' ).reduce ((sum, r ) => sum + r.size , 0 );
const cssSize = resources.filter (r => r.type === 'link' ).reduce ((sum, r ) => sum + r.size , 0 );
const imgSize = resources.filter (r => r.type === 'img' ).reduce ((sum, r ) => sum + r.size , 0 );
expect (timing.fcp ).toBeLessThan (budget.fcp );
expect (timing.ttfb ).toBeLessThan (budget.ttfb );
expect (timing.totalLoad ).toBeLessThan (budget.totalLoad );
expect (jsSize).toBeLessThan (budget.jsSize );
expect (cssSize).toBeLessThan (budget.cssSize );
expect (imgSize).toBeLessThan (budget.imageSize );
});
});
Profiling and Tracing
Capture Performance Trace test ('capture performance trace' , async ({ page, browser }) => {
await browser.startTracing (page, {
screenshots : true ,
categories : ['devtools.timeline' ],
});
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
const traceBuffer = await browser.stopTracing ();
require ('fs' ).writeFileSync ('trace.json' , traceBuffer);
});
Memory Profiling test ('check for memory leaks' , async ({ page }) => {
await page.goto ('/' );
const initialMemory = await page.evaluate (() => {
if (performance.memory ) {
return performance.memory .usedJSHeapSize ;
}
return 0 ;
});
for (let i = 0 ; i < 10 ; i++) {
await page.getByRole ('button' , { name : 'Open Modal' }).click ();
await page.getByRole ('button' , { name : 'Close' }).click ();
}
await page.evaluate (() => {
if (window .gc ) window .gc ();
});
const finalMemory = await page.evaluate (() => {
if (performance.memory ) {
return performance.memory .usedJSHeapSize ;
}
return 0 ;
});
const memoryGrowth = finalMemory - initialMemory;
expect (memoryGrowth).toBeLessThan (5 * 1024 * 1024 );
});
Best Practices
1. Run Performance Tests in Isolation
export default defineConfig ({
projects : [
{
name : 'performance' ,
testMatch : '**/*.perf.spec.ts' ,
use : {
video : 'off' ,
trace : 'off' ,
screenshot : 'off' ,
},
fullyParallel : false ,
},
],
});
2. Use Consistent Environment test.beforeEach (async ({ page }) => {
await page.context ().clearCookies ();
await page.route ('**/*' , route => {
if (route.request ().url ().includes ('sw.js' )) {
route.abort ();
} else {
route.continue ();
}
});
});
3. Test Critical User Paths test.describe ('Critical Path Performance' , () => {
test ('homepage to checkout' , async ({ page }) => {
const timings : Record <string , number > = {};
let start = Date .now ();
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
timings.homepage = Date .now () - start;
start = Date .now ();
await page.getByRole ('link' , { name : 'Featured Product' }).click ();
await page.waitForLoadState ('networkidle' );
timings.productPage = Date .now () - start;
start = Date .now ();
await page.getByRole ('button' , { name : 'Add to Cart' }).click ();
await page.waitForSelector ('[data-testid="cart-notification"]' );
timings.addToCart = Date .now () - start;
start = Date .now ();
await page.getByRole ('link' , { name : 'Checkout' }).click ();
await page.waitForLoadState ('networkidle' );
timings.checkout = Date .now () - start;
console .log ('Critical path timings:' , timings);
expect (timings.homepage ).toBeLessThan (3000 );
expect (timings.productPage ).toBeLessThan (2000 );
expect (timings.addToCart ).toBeLessThan (1000 );
expect (timings.checkout ).toBeLessThan (2000 );
});
});
Quick Reference
Key Metrics Metric Good Needs Improvement Poor LCP < 2.5s 2.5s - 4s > 4s FID < 100ms 100ms - 300ms > 300ms CLS < 0.1 0.1 - 0.25 > 0.25 FCP < 1.8s 1.8s - 3s > 3s TTFB < 600ms 600ms - 1.5s > 1.5s
Performance APIs
performance.getEntriesByType ('navigation' )
performance.getEntriesByType ('resource' )
performance.getEntriesByType ('paint' )
PerformanceObserver with type : 'longtask'
PerformanceObserver with type : 'layout-shift'
Related Resources