Testland
Browse all skills & agents

lighthouse-pwa-audit

Run and interpret Lighthouse PWA audits - even after the PWA *category* was deprecated per [developer.chrome.com/docs/lighthouse/pwa][lh-pwa], the individual audits (`installable-manifest`, `service-worker`, `splash-screen`, `themed-omnibox`, `viewport`, `content-width`, `apple-touch-icon`, `maskable-icon`) still run and report under a custom Lighthouse config or via direct audit invocation. Covers CLI flags (`--only-categories`, `--output`, `--form-factor`, `--throttling-method`), programmatic Node.js invocation, Lighthouse CI assertions (`categories:{id}`, `audit-id` thresholds), and LHR JSON parsing. Use when a manifest or icon change needs a precise installable-manifest verdict, or when CI must gate PRs on PWA audit scores despite the category badge being gone.

Install with skills.sh (any agent)

npx skills add testland/qa --skill lighthouse-pwa-audit
View source

lighthouse-pwa-audit

Overview

Lighthouse is the canonical PWA audit tool. The PWA category was deprecated per developer.chrome.com/docs/lighthouse/pwa (opens in new window), but the individual audits remain available and run on demand under a custom Lighthouse config. This skill covers running them and reading the LHR (Lighthouse Result) JSON they emit.

The companion @lhci/cli package per github.com/GoogleChrome/lighthouse-ci (opens in new window) gates CI: it wraps Lighthouse runs and asserts category / audit scores per a .lighthouserc.json config.

Pinned versions

Time-sensitive pins - re-check at the linked source on upgrade:

When to use

  • A PWA needs a pre-release audit gate covering installability, service-worker registration, and the maskable-icon contract.
  • A regression bisect points at a manifest / icon change and you need a precise installable-manifest verdict.
  • CI needs to fail PRs that drop below a fixed PWA-audit threshold even though the category badge is deprecated.
  • A site-reliability dashboard wants per-audit time-series - the LHR JSON is the canonical input.

Authoring

Step 1 - Install Lighthouse + Lighthouse CI

npm install --save-dev lighthouse @lhci/cli
# Or globally for one-off CLI use:
npm install -g lighthouse @lhci/cli

Step 2 - Inventory the PWA audits

Per lh-pwa (opens in new window), the audits previously grouped under the PWA category:

GroupAudit IDWhat it checks
Fast and reliableload-fast-enough-for-pwa"Page load speed on mobile networks" per lh-pwa (opens in new window)
Fast and reliableworks-offline"Current page responds with 200 when offline" per lh-pwa (opens in new window)
Fast and reliableoffline-start-url"start_url responds with 200 when offline" per lh-pwa (opens in new window)
Installableis-on-https"HTTPS requirement" per lh-pwa (opens in new window)
Installableservice-worker"Service worker registration controlling page and start_url" per lh-pwa (opens in new window)
Installableinstallable-manifest"Web app manifest installability requirements" per lh-pwa (opens in new window)
PWA optimizedredirects-http"HTTP to HTTPS redirect" per lh-pwa (opens in new window)
PWA optimizedsplash-screen"Custom splash screen configuration" per lh-pwa (opens in new window)
PWA optimizedthemed-omnibox"Theme color for address bar" per lh-pwa (opens in new window)
PWA optimizedcontent-width"Viewport sizing for content" per lh-pwa (opens in new window)
PWA optimizedviewport"Viewport meta tag presence" per lh-pwa (opens in new window)
PWA optimizedwithout-javascript"Fallback content without JavaScript" per lh-pwa (opens in new window)
PWA optimizedmaskable-icon"Maskable icon in manifest" per lh-pwa (opens in new window)
Manual(manual)"Cross-browser compatibility, network-independent page transitions, URL structure" per lh-pwa (opens in new window)

Step 3 - Run audits from the CLI

The basic invocation per lh-gh (opens in new window):

lighthouse https://localhost:3000 \
  --output=json \
  --output-path=./lhr.json \
  --form-factor=mobile \
  --throttling-method=simulate \
  --chrome-flags="--headless --window-size=412,660"

CLI flags from lh-gh (opens in new window):

FlagEffect
--output json / --output htmlOutput format(s); can pass multiple
--output-path=./lhr.jsonWrite to file (stdout by default)
--only-categories=pwaRestrict to category (still accepted even with PWA deprecated)
--only-audits=installable-manifest,service-workerRestrict to specific audits
--form-factor=mobile / desktopDevice emulation
--throttling-method=devtools / simulate / providedNetwork/CPU throttling mode
--chrome-flags="..."Pass-through to Chrome launcher

To restrict to the still-supported audits without invoking the deprecated category, list the audit IDs directly:

lighthouse https://localhost:3000 \
  --only-audits=installable-manifest,service-worker,maskable-icon,viewport,themed-omnibox,splash-screen,content-width,apple-touch-icon,is-on-https \
  --output=json \
  --output-path=./lhr.json

Step 4 - Author a Lighthouse CI config

Gate CI with .lighthouserc.json - assert per-audit IDs, not the deprecated categories:pwa. Installability audits at error, optimization audits at warn:

{
  "ci": {
    "collect": { "url": ["http://localhost:3000/"], "numberOfRuns": 3 },
    "assert": {
      "assertions": {
        "installable-manifest": ["error", { "minScore": 1 }],
        "service-worker": ["error", { "minScore": 1 }],
        "maskable-icon": ["error", { "minScore": 1 }],
        "themed-omnibox": ["warn", { "minScore": 1 }]
      },
      "aggregationMethod": "median-run"
    }
  }
}

Each assertion is [severity, { minScore | maxNumericValue | ... }] with severity one of off, warn, error; error fails the build, warn warns without failing. Use aggregationMethod: median-run for noisy mobile audits. The full config (all nine audits, onlyAudits, upload) is in references/lighthouse-config.md.

Step 5 - Programmatic invocation from Node.js

For per-test invocation outside Lighthouse CI, launch Chrome and read lhr.audits[id].score:

import lighthouse from 'lighthouse';
import { launch } from 'chrome-launcher';

const chrome = await launch({ chromeFlags: ['--headless'] });
const { lhr } = await lighthouse('http://localhost:3000/', {
  port: chrome.port, output: 'json', formFactor: 'mobile',
  onlyAudits: ['installable-manifest', 'service-worker', 'maskable-icon'],
} as any);
for (const id of ['installable-manifest', 'service-worker', 'maskable-icon']) {
  expect(lhr.audits[id].score).toBe(1);
}
await chrome.kill();

The lighthouse export resolves to { lhr, report, artifacts } - lhr is the parsed JSON, report the rendered HTML. The full Vitest spec is in references/lighthouse-config.md.

Running

Local one-off run

# Smoke
lighthouse https://localhost:3000/ \
  --only-audits=installable-manifest,service-worker,maskable-icon \
  --output=html --output-path=./pwa-smoke.html
open pwa-smoke.html

Lighthouse CI autorun

npm install -g @lhci/cli@0.15.x
lhci autorun

lhci autorun per lhci-gh (opens in new window) is the umbrella command that "orchestrates the workflow" - it sequences lhci collect lhci assertlhci upload.

CI (GitHub Actions)

The lhci autorun workflow (checkout -> setup-node -> build -> lhci autorun) is in references/lighthouse-config.md. The ## CI integration section below adds the upload-on-failure step.

Parsing results

The LHR (Lighthouse Result) JSON is the canonical machine-readable output. Key paths:

PathWhat it holds
lhr.audits.<audit-id>.score0..1 (or null if not applicable); 1 = pass
lhr.audits.<audit-id>.displayValueHuman-readable summary string
lhr.audits.<audit-id>.detailsPer-audit details object (table / list shape varies)
lhr.audits.<audit-id>.scoreDisplayModenumeric, binary, informative, manual, notApplicable, error
lhr.categories.<cat-id>.scoreAggregate category score 0..1 (PWA category present but deprecated per lh-pwa (opens in new window))
lhr.runWarningsArray of run-time warnings the audit emitted
lhr.lighthouseVersionWhich Lighthouse version produced this LHR

Per lh-gh (opens in new window), the LHR schema is stable across patch releases; major releases may add / remove audits. Pin the Lighthouse version (see Pinned versions) in CI for stable assertions.

For the installable-manifest audit specifically, the details field contains a list of failing requirements (e.g. "Manifest does not have a maskable icon", "Page does not work offline"). A failed-audit triage step is:

const a = lhr.audits['installable-manifest'];
if (a.score !== 1) {
  console.log('installable-manifest failures:');
  for (const item of a.details?.items ?? []) {
    console.log('  -', item.failureReason || item.message || JSON.stringify(item));
  }
}

CI integration

For projects that ship a PWA: gate PRs on the still-supported install audits as a baseline.

jobs:
  pwa-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
      - run: npm ci && npm run build
      - run: npm install -g @lhci/cli@0.15.x
      - run: lhci autorun
      - if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: lhr-failure
          path: .lighthouseci/

The upload-artifact-on-failure step is essential - lhci's default output is a temporary-public-storage URL that disappears after the job retention window. Persisting the .lighthouseci/ directory gives engineers the LHR JSON to triage offline.

Anti-patterns

Anti-patternWhy it failsFix
Assert categories:pwa minScore in lighthousercThe PWA category is deprecated per lh-pwa (opens in new window); future Lighthouse majors may drop it entirelyAssert per-audit IDs in Step 4 instead
Run Lighthouse in default desktop form-factor against a mobile PWADifferent audits gate; splash-screen and themed-omnibox are mobile-specific per lh-pwa (opens in new window)--form-factor=mobile (Step 3)
Single Lighthouse run per CI jobPer-run variance is ±5 points; one bad run fails CInumberOfRuns: 3 + aggregationMethod: median-run per lhci-config (opens in new window) (Step 4)
Mix Lighthouse versions across CI runsAudit weights / IDs shift across majors; time-series breaksPin @lhci/cli@<major>.<minor> (Step 4)
Treat score: null as failingnull means "not applicable" per the LHR schemaFilter score === null before threshold check
Skip --throttling-method=devtools and use providedprovided makes Lighthouse trust whatever throttling the harness sets - usually nothing, inflating scoressimulate for repeatable CI; devtools for actual-CPU runs (Step 3)
Run Lighthouse against https://localhost:3000 with self-signed certLighthouse rejects; LHR contains runtimeErrorPass --chrome-flags="--ignore-certificate-errors" or run on plain HTTP locally

Limitations

  • The PWA category is deprecated per lh-pwa (opens in new window); the individual audits work but the aggregate category badge is no longer surfaced in Chrome DevTools UI. Asserting per-audit (Step 4) is the forward-compatible posture.
  • load-fast-enough-for-pwa and works-offline scores depend on the network throttling profile; cross-environment comparisons require pinning --throttling-method and --form-factor.
  • Per-run variance. Even with median-run aggregation, mobile emulation introduces ±5-point swings on perf metrics; installable-manifest and service-worker are binary and stable, but other audits drift.
  • Headless CI cannot test iOS install criteria. Lighthouse drives Chromium only; the apple-touch-icon audit checks the link tag presence, not the resulting iOS install behavior. See pwa-install-flow-reference Stage 3 for the iOS-specific path.
  • Authenticated routes require the auth recipe from lh-gh (opens in new window) docs - programmatic invocation with a logged-in Chrome user data dir, not covered by the default lighthouse <url> form.

References

  • Lighthouse PWA audits (deprecation notice, audit list, per-audit purpose statements) - lh-pwa (opens in new window).
  • Lighthouse repo (v13.3.0, CLI flags, --only-categories, --throttling-method, programmatic API) - lh-gh (opens in new window).
  • Lighthouse CI repo (lhci autorun, GitHub Actions workflow) - lhci-gh (opens in new window).
  • Lighthouse CI config (.lighthouserc.json shape, preset values, category vs audit assertions, aggregation methods) - lhci-config (opens in new window).
  • Differentiation: this skill is the audit reader. The pwa-install-flow-reference is the contract the installable-manifest audit checks against; the workbox-tests skill covers the runtime cache behavior Lighthouse can't fully inspect.
  • Sibling skills: pwa-install-flow-reference, web-push-tests, service-worker-lifecycle-tests.

Lighthouse CI config and programmatic invocation

View source (opens in new window)

Lighthouse CI config and programmatic invocation

Companion detail for lighthouse-pwa-audit. The inline CLI run (Step 3) is enough to produce an LHR; use these when gating CI or invoking Lighthouse from a test runner.

Lighthouse CI config (.lighthouserc.json)

Full config for all nine still-supported audits:

{
  "ci": {
    "collect": {
      "url": ["http://localhost:3000/"],
      "numberOfRuns": 3,
      "settings": {
        "onlyAudits": [
          "installable-manifest",
          "service-worker",
          "maskable-icon",
          "viewport",
          "themed-omnibox",
          "splash-screen",
          "content-width",
          "apple-touch-icon",
          "is-on-https"
        ],
        "throttlingMethod": "devtools"
      }
    },
    "assert": {
      "assertions": {
        "installable-manifest": ["error", { "minScore": 1 }],
        "service-worker": ["error", { "minScore": 1 }],
        "maskable-icon": ["error", { "minScore": 1 }],
        "viewport": ["error", { "minScore": 1 }],
        "is-on-https": ["error", { "minScore": 1 }],
        "themed-omnibox": ["warn", { "minScore": 1 }],
        "splash-screen": ["warn", { "minScore": 1 }],
        "content-width": ["warn", { "minScore": 1 }],
        "apple-touch-icon": ["warn", { "minScore": 1 }]
      },
      "aggregationMethod": "median-run"
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

Per lhci-config, each assertion is "<audit-id-or-categories:<id>>": [severity, { minScore | maxNumericValue | ... }] with severity one of off, warn, error. error fails the build; warn surfaces a warning without failing. aggregationMethod supports median, optimistic, pessimistic, median-run; median-run "represents the most typical run" and suits noisy mobile PWA audits.

Programmatic invocation from Node.js (full Vitest spec)

// tests/lighthouse-pwa.spec.ts
import { test, expect } from 'vitest';
import lighthouse from 'lighthouse';
import { launch } from 'chrome-launcher';

test('PWA audits pass on the build', async () => {
  const chrome = await launch({ chromeFlags: ['--headless'] });
  try {
    const { lhr } = await lighthouse(
      'http://localhost:3000/',
      {
        port: chrome.port,
        output: 'json',
        onlyAudits: [
          'installable-manifest',
          'service-worker',
          'maskable-icon',
          'viewport',
          'is-on-https',
        ],
        formFactor: 'mobile',
        throttlingMethod: 'devtools',
      } as any
    );

    for (const id of [
      'installable-manifest',
      'service-worker',
      'maskable-icon',
      'viewport',
      'is-on-https',
    ]) {
      expect(lhr.audits[id].score).toBe(1);
    }
  } finally {
    await chrome.kill();
  }
});

The lighthouse npm export returns a Promise resolving to { lhr, report, artifacts }. The LHR is the parsed JSON; report is the rendered HTML (when requested).

Lighthouse CI in GitHub Actions

The lhci autorun umbrella command sequences collect -> assert -> upload:

name: CI
on: [push]
jobs:
  lighthouseci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install && npm install -g @lhci/cli@0.15.x
      - run: npm run build
      - run: lhci autorun

Source references:

  • lh-gh: https://github.com/GoogleChrome/lighthouse
  • lhci-gh: https://github.com/GoogleChrome/lighthouse-ci
  • lhci-config: https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md

Related skills

add-to-homescreen-flow-tests

Build-an-X workflow that emits the Add-to-Home-Screen / install-flow test suite. Walks the four-stage install timeline (gate → `beforeinstallprompt` handshake → per-platform path → `display-mode` MQ), emits one test per gate cell per [web.dev/articles/install-criteria][install-criteria], the deferred-prompt → `prompt()` → `userChoice` chain per [web.dev/articles/customize-install][customize-install], the iOS Safari manual-metadata branch (`apple-touch-icon`, `apple-mobile-web-app-capable`) per [web.dev/learn/pwa/installation][learn-pwa], and the post-install `(display-mode: standalone)` MQ assertion. Output: a Playwright spec file with per-stage cells plus the iOS metadata spec, plus a coverage matrix mapping each install criterion to its assertion. Use when a PWA's manifest, icons, or install handler change, or when install conversion drops and it is unclear which install stage users fall out at.

offline-fallback-tests

Build-an-X workflow that emits the offline-fallback test suite. Walks the eight Jake Archibald offline recipes per [web.dev/articles/offline-cookbook][off-cookbook] (`Cache only`, `Network only`, `Cache, falling back to network`, `Cache and network race`, `Network falling back to cache`, `Cache then network`, `Generic fallback`, `Service Worker side templating`), maps each recipe to its assertion shape, layers the Workbox `offlineFallback()` recipe per [developer.chrome.com/docs/workbox/modules/workbox-recipes][wb-recipes], and pins the offline storage strategy (Cache Storage vs IndexedDB vs Storage Manager `persist()`/`estimate()`) per [web.dev/learn/pwa/offline-data][off-data]. Output: a Playwright spec file with one test per route's chosen recipe + a coverage matrix mapping recipes to URL patterns. Use when a route's SW caching behavior is being chosen or changed, or when a "doesn't work offline" report needs the failing route's recipe localized.

pwa-install-flow-reference

Pure reference for the PWA install flow as a test surface - the installability gate (manifest required fields per [install-criteria], registered service worker, HTTPS, ~30s user engagement), the `beforeinstallprompt` handshake (preventDefault → stash → prompt() on gesture → userChoice → appinstalled), and the `display-mode` post-install signal, with a how-to-use walkthrough and a worked Chromium install-flow assertion; per-platform paths (Android WebAPK, iOS Share menu, Firefox no-op) live in references/. Use when authoring or triaging install-flow assertions and you need the gate fields, the event contract, and the per-platform expectations in one place instead of re-reading three vendor docs.

service-worker-lifecycle-tests

Build-an-X workflow that emits per-SW state-transition tests covering the six `ServiceWorkerState` values per [w3c-github-io/ServiceWorker][sw-spec] (`parsed → installing → installed → activating → activated → redundant`), the `install` / `activate` / `fetch` event handlers per [MDN Service Worker API][mdn-sw], `event.waitUntil()` lifetime extension, `ServiceWorkerGlobalScope.skipWaiting()` and `Clients.claim()` upgrade-path semantics, the `statechange` event on `ServiceWorker` objects, `ServiceWorkerRegistration.update()`, and `navigator.serviceWorker.controller` checks. Output: a Playwright spec file with one test per transition plus a clean upgrade-path test (v1 active → v2 installed/waiting → v2 activated, with claim()). Use when authoring the baseline lifecycle spec, or when a deploy leaves users stuck on the old service worker - scoped to the state machine and the `skipWaiting` / `clients.claim` upgrade path, not to general service-worker assertion or cache-strategy patterns.

web-push-tests

Test the browser web-push subscription lifecycle - `pushManager.subscribe({ userVisibleOnly, applicationServerKey })` per [W3C Push API][w3c-push] returning a `PushSubscription` with `endpoint` + `keys.p256dh` + `keys.auth` + optional `expirationTime`; the `pushsubscriptionchange` service-worker event on refresh / revoke / expiry; the `push` event delivery with `PushMessageData`; VAPID auth per RFC 8292 (ES256 JWT, `aud` / `exp` ≤ 24h / `sub`); RFC 8030 push-service responses (201 Created, 410 Gone for expired endpoints, 413 Payload Too Large, 429); and `unsubscribe()` cleanup. Use when a PWA ships web-push and the subscription lifecycle needs release-gate coverage - scoped to the browser Push API, not to cross-channel delivery over native APNs / FCM.

workbox-tests

Test Workbox-built service workers - pin behavior of the named recipes (`pageCache`, `staticResourceCache`, `imageCache`, `googleFontsCache`, `offlineFallback`, `warmStrategyCache`) per [developer.chrome.com/docs/workbox/modules/workbox-recipes][wb-recipes]; validate `workbox-precaching` manifest injection (`__WB_MANIFEST` revisioning); assert `workbox-routing` route handler matches; assert `workbox-expiration` and `workbox-cacheable-response` plugin gates; and verify the `workbox-window` registration helper events (`installed`, `waiting`, `controlling`, `activated`). Use when a project already ships a Workbox-built service worker and its recipe behavior needs pinning against refactors or a version upgrade.