ScreenshotNeo

BlogHow-to

How to Collect CSS Coverage in Puppeteer

Learn to collect CSS coverage in Puppeteer, measure covered stylesheet ranges, exercise interactive states, and understand navigation and reporting caveats.

By the ScreenshotNeo team4 October 20266 min read

To collect CSS coverage in Puppeteer, start coverage before navigating or exercising the page, then call page.coverage.stopCSSCoverage() to retrieve stylesheet entries. Each entry includes the stylesheet URL, its source text, and the covered ranges. Coverage reflects only the states your run actually exercised, and by default it resets on navigation.

Collect CSS coverage

Use this runnable example to measure the stylesheet ranges used during a page load. Install Puppeteer in your project with npm install puppeteer, then save the code as coverage.js and run node coverage.js.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();

    // Start before navigation so the initial page load is included.
    await page.coverage.startCSSCoverage();
    await page.goto('https://example.com', { waitUntil: 'networkidle0' });

    // Exercise any relevant states before stopping coverage.
    // Example: await page.click('[aria-expanded="false"]');

    const cssCoverage = await page.coverage.stopCSSCoverage();

    for (const entry of cssCoverage) {
      const usedBytes = entry.ranges.reduce(
        (sum, range) => sum + range.end - range.start - 1,
        0,
      );
      const totalBytes = entry.text.length;
      const percent = totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100;

      console.log({
        url: entry.url,
        usedBytes,
        totalBytes,
        percentUsed: Number(percent.toFixed(2)),
        ranges: entry.ranges,
      });
    }
  } finally {
    await browser.close();
  }
})();

The range calculation follows Puppeteer’s example: sum end - start - 1 for each covered range. Comparing that sum with entry.text.length gives a simple coverage percentage. Treat the result as a measurement of this run, not proof that unvisited CSS is unnecessary.

Exercise the page states you need to measure

Coverage records CSS use during the work performed while collection is active. If you want to know which styles are used by a menu, tab, modal, or other interactive state, trigger that state before stopping coverage. A run that only loads the initial page cannot establish whether styles used by an unopened dialog are unused.

  1. Start CSS coverage before the navigation or interaction sequence you want to include.
  2. Navigate to the page and wait for the page state relevant to your measurement.
  3. Exercise representative interactions, such as opening menus, changing tabs, and displaying dialogs.
  4. Stop collection after those states have been visited, then inspect the returned entries.

For repeatable reports, make the interaction sequence deterministic and document which states were included. Consider separate runs for materially different routes or user states so each result has a clear scope.

Understand the returned entries and ranges

Each CSS coverage entry exposes url, text, and ranges. The URL identifies the stylesheet, the text contains its source, and each range has start and end positions for covered source. Use the stylesheet URL and ranges when producing a per-file report or tracing a result back to source.

The byte percentage is a useful summary, but it is run-specific. It depends on the routes loaded, interactions performed, and navigation behavior during the run. It is not a universal statement about every possible page state or user journey.

Coverage resets on navigation by default

Puppeteer’s documented default for resetOnNavigation is true. Account for this when a flow navigates between documents: coverage collected before a navigation may be reset. Set the option deliberately when your report needs to include navigation behavior, and confirm the behavior against the Puppeteer version used by your project.

await page.coverage.startCSSCoverage({ resetOnNavigation: false });

Use this setting only when it matches the question your report is meant to answer. For single-document page measurements, the default is often the intended scope.

Injected style tags without source URLs can be missing

Puppeteer documents that CSS coverage does not include dynamically injected style tags that lack sourceURLs. If your application builds styles at runtime, treat coverage as incomplete for that styling path unless those styles carry source URLs.

Coverage depends on executed behavior

Chrome’s Coverage guidance describes beginning recording with a reload and continuing while interacting with the page. The same practical principle applies here: exercise the states relevant to your question before stopping collection. Unvisited states can leave their CSS ranges uncovered in the report.

Options and reporting choices

Choice Effect When to consider it
resetOnNavigation Controls whether coverage resets on navigation; documented default is true. Flows that move between documents and reports that span navigation.
Interaction sequence Determines which CSS ranges can be observed as used. Any page with menus, tabs, dialogs, conditional content, or other interactive states.
Measurement boundary Starting and stopping defines the activity included in the result. Comparing routes, states, or application revisions consistently.

Keep raw entries or a machine-readable summary alongside any aggregate percentage when you need to investigate which file or range contributed to the result.

Troubleshooting

Symptom Likely cause Fix
Coverage output is empty or has no useful stylesheet entries. Collection began after the relevant load, or the page did not load the expected stylesheets. Start collection before navigation, verify the page and stylesheets load, then repeat the intended interaction sequence.
A menu or dialog’s styles appear uncovered. The state was not opened while coverage was active. Trigger the state before calling stopCSSCoverage().
Coverage from an earlier page is missing after navigation. resetOnNavigation defaults to true. For a report spanning navigation, configure resetOnNavigation: false and verify the chosen scope.
Runtime-generated CSS is absent. Injected style tags without sourceURLs are excluded from CSS coverage. Use source URLs for injected styles where possible, and account for this limitation when interpreting results.
Reported unused CSS is still needed in production. The run did not exercise every relevant route, state, or user journey. Expand the scenarios and treat uncovered ranges as candidates for review, not automatic deletion instructions.

Performance, reliability, and cost

Coverage adds instrumentation to a browser run, so collect it in the automation workflow where you need the data and keep the measured scenario consistent. The research sources provide no benchmark for its overhead; measure impact in your own run if timing matters. Reliability depends on reaching the intended page state and completing the interactions before stopping. Puppeteer coverage itself has no per-capture service charge; browser compute and CI time are determined by your execution environment.

Or skip the browser setup

If your task is to capture a page image rather than inspect stylesheet ranges, ScreenshotNeo provides a website screenshot API. It does not collect CSS coverage; it returns a screenshot or PDF. The API can accept a URL in one request. See the ScreenshotNeo API docs for the available options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = require('node:fs');
fs.writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
  • An MCP server lets AI agents use tools for screenshots, page information, and PDF capture.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Does CSS coverage identify all unused CSS?

No. It reports ranges not observed as used in the run. A range may be needed by a state the run did not visit.

Can I use coverage for JavaScript too?

This guide covers CSS collection with startCSSCoverage() and stopCSSCoverage(). Puppeteer’s official example also demonstrates JavaScript coverage, but that is a separate collection API.

Does CSS coverage include inline runtime styles?

Not when they are dynamically injected style tags without sourceURLs, according to Puppeteer’s documented caveat.

Sources