This recipe covers spot-checking web performance with Lighthouse. For operationalized performance monitoring — scheduled runs, trend tracking, aggregated reports, and alerting — talk to your QA Wolf team about full-service performance testing.
Examples
Run a Lighthouse audit and assert on the performance score:
import { expect, flow } from "@qawolf/flows/web";
import { playAudit } from "playwright-lighthouse";
const PERF_SCORE_MIN = 80;
export default flow(
"Homepage performance",
"Web - Chrome",
async ({ test, ...testContext }) => {
await test("Lighthouse audit", async () => {
const { launch } = testContext;
const { context } = await launch();
context.setDefaultTimeout(8000);
const page = await context.newPage();
await page.goto(process.env.BASE_URL);
await page.waitForLoadState("networkidle").catch(() => {});
const { lhr } = await playAudit({
page,
thresholds: { performance: PERF_SCORE_MIN },
config: {
extends: "lighthouse:default",
settings: { onlyCategories: ["performance"] },
},
});
const perfScore = Math.round(lhr.categories.performance.score * 100);
expect(perfScore).toBeGreaterThanOrEqual(PERF_SCORE_MIN);
});
},
);
Assert on individual Core Web Vitals:
const lcp = lhr.audits["largest-contentful-paint"].numericValue;
const fcp = lhr.audits["first-contentful-paint"].numericValue;
const tbt = lhr.audits["total-blocking-time"].numericValue;
const cls = lhr.audits["cumulative-layout-shift"].numericValue;
const tti = lhr.audits["interactive"].numericValue;
const ttfb = lhr.audits["server-response-time"].numericValue;
Save the audit report to team storage:
const { lhr } = await playAudit({
page,
thresholds: { performance: PERF_SCORE_MIN },
reports: {
formats: { json: true },
directory: `${process.env.TEAM_STORAGE_DIR}/lighthouse`,
name: `homepage-${Date.now()}`,
},
config: {
extends: "lighthouse:default",
settings: { onlyCategories: ["performance"] },
},
});
The double navigation is intentional. Lighthouse measures the second load so that cookie banner dismissal steps don’t skew the metrics.
When to use
- Your team wants a quick sanity check that a page meets minimum performance thresholds before or after a deploy
- You want to catch regressions in Core Web Vitals — LCP, CLS, or FCP — introduced by new code
- You need a lightweight performance gate in CI without a full monitoring setup
- You want to verify that a specific page (checkout, landing page, dashboard) hasn’t degraded after a significant change
For more on what each metric means and why it matters, see Web performance metrics, explained.
Notes
- The sample test below runs in Google Chrome only, using port
9222, which is the Chrome DevTools Protocol port. Lighthouse connects to an already-running Chrome instance via CDP rather than launching its own.
playAudit requires a Playwright page object with an active navigation. Call it after waitForLoadState to ensure the page has settled.
- Lighthouse runs in a simulated environment — results will vary slightly between runs. Avoid very tight thresholds (e.g.
≤ 100ms for TBT) that will produce flaky results.
- Thresholds are declared as named constants at the top of the file so they are easy to find and adjust per environment.
onlyCategories: ["performance"] scopes the audit to performance only. Lighthouse can also score accessibility and SEO.
Full sample test
import { expect, flow } from "@qawolf/flows/web";
import { playAudit } from "playwright-lighthouse";
// Thresholds based on qawolf.com/blog/web-performance-metrics-explained
const PERF_SCORE_MIN = 80;
const LCP_MAX_MS = 2500;
const FCP_MAX_MS = 1800;
const TBT_MAX_MS = 200;
const CLS_MAX = 0.1;
const TTI_MAX_MS = 3000;
const TTFB_MAX_MS = 150;
const PAGE_LOAD_MAX_MS = 2000;
export default flow(
"Homepage",
"Web - Chrome",
async ({ test, ...testContext }) => {
await test("Performance test for homepage", async () => {
const { launch } = testContext;
//--------------------------------
// Arrange: Launch browser and navigate to homepage
//--------------------------------
const { context } = await launch({ args: ["--remote-debugging-port=9222"] });
context.setDefaultTimeout(8000);
const page = await context.newPage();
await page.goto(process.env.BASE_URL);
//--------------------------------
// Act: Run Lighthouse performance audit
//--------------------------------
await page.goto(process.env.BASE_URL);
await page.waitForLoadState("networkidle").catch(() => {});
const { lhr } = await playAudit({
page,
port: 9222,
thresholds: {
performance: PERF_SCORE_MIN,
},
reports: {
formats: { json: true },
directory: `${process.env.TEAM_STORAGE_DIR}/lighthouse`,
name: `homepage-${Date.now()}`,
},
config: {
extends: "lighthouse:default",
settings: {
onlyCategories: ["performance"],
},
},
});
//--------------------------------
// Assert: Verify all performance metrics are within budget
//--------------------------------
const perfScore = Math.round(lhr.categories.performance.score * 100);
const lcp = lhr.audits["largest-contentful-paint"].numericValue;
const fcp = lhr.audits["first-contentful-paint"].numericValue;
const tbt = lhr.audits["total-blocking-time"].numericValue;
const cls = lhr.audits["cumulative-layout-shift"].numericValue;
const tti = lhr.audits["interactive"].numericValue;
const ttfb = lhr.audits["server-response-time"].numericValue;
const pageLoadTime = lhr.audits["metrics"].details.items[0].observedLoad;
console.log(`Performance score: ${perfScore} (min ${PERF_SCORE_MIN})`);
console.log(`LCP: ${Math.round(lcp)}ms (max ${LCP_MAX_MS}ms)`);
console.log(`FCP: ${Math.round(fcp)}ms (max ${FCP_MAX_MS}ms)`);
console.log(`TBT: ${Math.round(tbt)}ms (max ${TBT_MAX_MS}ms)`);
console.log(`CLS: ${cls.toFixed(3)} (max ${CLS_MAX})`);
console.log(`TTI: ${Math.round(tti)}ms (max ${TTI_MAX_MS}ms)`);
console.log(`TTFB: ${Math.round(ttfb)}ms (max ${TTFB_MAX_MS}ms)`);
console.log(`Page load: ${Math.round(pageLoadTime)}ms (max ${PAGE_LOAD_MAX_MS}ms)`);
expect(perfScore).toBeGreaterThanOrEqual(PERF_SCORE_MIN);
expect(lcp).toBeLessThanOrEqual(LCP_MAX_MS);
expect(fcp).toBeLessThanOrEqual(FCP_MAX_MS);
expect(tbt).toBeLessThanOrEqual(TBT_MAX_MS);
expect(cls).toBeLessThanOrEqual(CLS_MAX);
expect(tti).toBeLessThanOrEqual(TTI_MAX_MS);
expect(ttfb).toBeLessThanOrEqual(TTFB_MAX_MS);
expect(pageLoadTime).toBeLessThanOrEqual(PAGE_LOAD_MAX_MS);
});
},
);