ScreenshotNeo

BlogHow-to

How to Start CSS Coverage in Puppeteer

Start CSS coverage before navigation, exercise the page states you care about, then stop and inspect the stylesheet reports.

By the ScreenshotNeo team4 October 20267 min read

Start CSS coverage on the Puppeteer Page before navigating to the page or performing the interactions you want to measure. After those actions, call page.coverage.stopCSSCoverage() to receive an array of stylesheet reports.

await page.coverage.startCSSCoverage();
await page.goto('https://example.com');
// Exercise the page states you want to include.
const cssCoverage = await page.coverage.stopCSSCoverage();

This guide uses Puppeteer’s documented page.coverage API. See the startCSSCoverage() reference and stopCSSCoverage() reference for the current signatures and caveats.

1. Install Puppeteer and collect CSS coverage

In a new Node.js project, install Puppeteer:

npm install puppeteer

Save the following as coverage.mjs and run it with node coverage.mjs. It starts coverage before navigation, waits for the page load event, stops collection, and prints each stylesheet’s URL and reported ranges.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();

  // Coverage must be active before the activity you want to measure.
  await page.coverage.startCSSCoverage();
  await page.goto('https://example.com', { waitUntil: 'load' });

  // Perform any interactions whose CSS states should count here.
  // For example: await page.click('button.menu');

  const reports = await page.coverage.stopCSSCoverage();
  for (const report of reports) {
    console.log({ url: report.url, ranges: report.ranges });
  }
} finally {
  await browser.close();
}

The finally block closes the browser even if navigation or collection fails. If you need the stylesheet text for analysis, each entry also includes text.

2. Understand the report

stopCSSCoverage() resolves to an array of reports, one entry for each stylesheet represented in the capture. A coverage entry has a stylesheet URL, its text, and an array of ranges with start and end offsets. Those offsets refer to positions in the stylesheet text.

Puppeteer’s documentation shows a byte-style percentage calculation: total the stylesheet text lengths, sum range.end - range.start - 1 for used ranges, then divide used by total. Treat this as a rough coverage calculation over the returned text; it is not a universal measure of stylesheet quality, visual correctness, or page performance.

function summarizeCssCoverage(entries) {
  let totalBytes = 0;
  let usedBytes = 0;

  for (const entry of entries) {
    totalBytes += entry.text.length;
    for (const range of entry.ranges) {
      usedBytes += range.end - range.start - 1;
    }
  }

  return {
    totalBytes,
    usedBytes,
    percentUsed: totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100,
  };
}

// After stopCSSCoverage():
console.log(summarizeCssCoverage(reports));

This follows the calculation in the Puppeteer Coverage class documentation. Do not interpret the percentage as a recommendation to delete every unreported rule: the result only reflects the captured browsing path and runtime state.

3. Choose the collection window and navigation behavior

Start before the page activity of interest

Start before navigation when measuring initial page rendering. If the page is already loaded, starting coverage measures only subsequent activity, not CSS usage that occurred before collection began.

Exercise meaningful states before stopping

Coverage records styles used during the collection window. Open menus, dialogs, tabs, expanded sections, or other states whose styles matter before calling stopCSSCoverage(). A single initial load cannot tell you whether a rule is needed in interactions you never performed.

The optional resetOnNavigation setting defaults to true:

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

You can set it to false when you want to change the default reset behavior:

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

Do not assume that setting it to false guarantees coverage survives a navigation. When you need reliable per-page results across multiple documents, stop collection before navigating away, save that report, then start a new collection on the next page. The documented option and default are listed in the API reference.

4. Capture multiple routes or states

For a site audit, collect one report per route or scenario. This makes it clear which navigation and interaction sequence produced each result and avoids relying on coverage that may have been reset or discarded when the page changed.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const urls = ['https://example.com/', 'https://example.com/pricing'];
  const allReports = [];

  for (const url of urls) {
    await page.coverage.startCSSCoverage();
    await page.goto(url, { waitUntil: 'load' });

    // Add route-specific interactions here before stopping.
    const reports = await page.coverage.stopCSSCoverage();
    allReports.push({ url, reports });
  }

  for (const route of allReports) {
    console.log(route.url, summarizeCssCoverage(route.reports));
  }
} finally {
  await browser.close();
}

function summarizeCssCoverage(entries) {
  let totalBytes = 0;
  let usedBytes = 0;
  for (const entry of entries) {
    totalBytes += entry.text.length;
    for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
  }
  return { totalBytes, usedBytes, percentUsed: totalBytes ? usedBytes / totalBytes * 100 : 0 };
}

For an interaction-heavy application, keep the collection active while exercising several states on the same document, then stop and label the result with the scenario. If an action navigates to another document, prefer stopping and restarting around that navigation so each report has a clear scope.

5. Options, limitations, and downstream formats

Choice or field What it means Practical use
resetOnNavigation Optional boolean; defaults to true. Keep the default for ordinary single-page captures. Stop and restart around document navigations when you need distinct route reports.
url The stylesheet URL associated with an entry. Use it to identify the source stylesheet. Inline styles may not have a useful remote stylesheet URL.
text The stylesheet content used for range offsets. Retain it with the report if you plan to inspect or map ranges later.
ranges Start/end offsets for reported used sections. Use the ranges against the matching text, not against a separately transformed or minified copy.

Puppeteer documents this caveat: “CSS Coverage doesn’t include dynamically injected style tags without sourceURLs.” Therefore the report is not a complete inventory of every CSS rule present at runtime. If your application injects CSS dynamically, account for that limitation when interpreting results.

If a downstream workflow needs Istanbul-compatible output, Puppeteer points to puppeteer-to-istanbul. It is optional; it is not required to start coverage or receive Puppeteer’s reports.

6. Troubleshooting

Symptom Likely cause Fix
The report is empty or missing expected rules Coverage started after the page activity, or the relevant state was never exercised. Start before navigation and reproduce the interaction before stopping.
A stylesheet or state disappears after navigation The default reset behavior is enabled, or the browser discarded the prior document context. Stop and save coverage before navigating, then start a fresh collection for the next page.
Injected component styles are absent Dynamically injected style tags without sourceURL annotations are excluded by the documented caveat. Do not treat the report as complete for those styles; add source annotations in systems you control or measure them through an appropriate application-level method.
Coverage starts but there are few used ranges The capture observed only a narrow set of rendered states. Exercise relevant menus, breakpoints, dialogs, and routes. Repeat with the viewport and state combinations that matter to your audit.
The byte percentage looks implausible The calculation is over returned text and captured execution, and may not reflect your intended quality metric. Check that totals are nonzero, ranges are applied to the corresponding text, and the capture covers the intended route and interactions.
Navigation throws or times out before results are printed The navigation did not reach the chosen lifecycle condition or the target is unavailable. Handle navigation errors, choose an appropriate waitUntil condition for the page, and ensure cleanup still stops collection or closes the browser.

7. Performance, reliability, and cost

Coverage adds browser instrumentation and produces report data proportional to the stylesheets encountered. The reviewed Puppeteer documentation does not provide a performance benchmark or quantify overhead, so measure it in your own capture environment if timing matters. Keep the capture window limited to the routes and interactions you intend to analyze, and avoid retaining large report objects longer than needed.

For repeatable results, record the URL, viewport, interaction sequence, and collection boundaries alongside each report. Dynamic content, responsive breakpoints, authentication state, and navigation can change which styles are used. A single run is evidence about that run’s path through the page, not a guarantee that all visitors use the same styles.

CSS coverage is computed locally by Puppeteer and does not require buying a product or sending the report to a service. Browser runtime and infrastructure costs depend on where and how you run Puppeteer; the source material does not establish a specific cost or speed figure.

8. Or skip the browser setup

If your goal is to capture the rendered page as an image or PDF rather than analyze used CSS ranges, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call endpoint returns a screenshot or PDF; it is not a CSS coverage collector.

See the ScreenshotNeo API documentation for request options. cURL example:

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

Python:

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)

Node.js:

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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
  • Cookie banners, popups, and chat widgets are removed before the shot.
  • Bot checks, blank pages, and failed loads are never billed.
  • An MCP server lets AI agents take screenshots.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month with no card.

9. FAQ

Can I use CSS coverage to decide what styles to delete?

Use it to identify styles not observed in your captured scenarios, then validate against the routes, viewport sizes, and interactions your product supports before removing anything.

Does stopping coverage navigate or reload the page?

stopCSSCoverage() returns the collected reports and ends collection; it is not a substitute for navigating to another route.

Can I combine CSS and JavaScript coverage?

Yes. Puppeteer’s Coverage documentation demonstrates starting both collectors and stopping both, but the JavaScript collector has its own options and caveats.

Where can I send the result for Istanbul?

Puppeteer names puppeteer-to-istanbul as an optional path for Istanbul-consumable output.