Skip to main content
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

Performance thresholds

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);
    });
  },
);
Last modified on April 17, 2026