How to Measure JavaScript and CSS Coverage with Puppeteer
Measure JavaScript and CSS usage during a defined page load or interaction sequence with Puppeteer, calculate used bytes, and understand what the report leaves out.
To measure JavaScript and CSS coverage with Puppeteer, start both coverage collectors before the page load or interactions you want to measure, perform that work, then stop the collectors. Sum the source text lengths for total bytes and the reported used ranges for used bytes. The result describes only the code Puppeteer observed during that measurement window and under the options you chose.
Puppeteer’s Coverage API gathers information about JavaScript and CSS used by a page. The example below follows its combined byte-percentage approach and adds a guard for an empty report.
1. Set up a repeatable measurement
Install Puppeteer in a Node.js project:
npm install puppeteer
Save the following as coverage.js. It starts JavaScript and CSS coverage before navigation, waits for the page to load, runs a clearly marked place for your interactions, collects both reports, and prints separate and combined percentages.
const puppeteer = require('puppeteer');
function summarize(entries) {
let totalBytes = 0;
let usedBytes = 0;
for (const entry of entries) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
// Puppeteer's documented range calculation excludes one boundary byte.
usedBytes += range.end - range.start - 1;
}
}
return {
totalBytes,
usedBytes,
percent: totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100,
};
}
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage(),
]);
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// Exercise the page paths that define this run's coverage.
// Example: await page.click('[data-testid="menu"]');
// Example: await page.waitForSelector('[data-testid="menu-panel"]');
const [jsEntries, cssEntries] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage(),
]);
const js = summarize(jsEntries);
const css = summarize(cssEntries);
const combined = summarize([...jsEntries, ...cssEntries]);
console.log(JSON.stringify({
javascript: js,
css,
combined,
scriptsReported: jsEntries.length,
stylesheetsReported: cssEntries.length,
}, null, 2));
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with:
node coverage.js
networkidle2 is a convenient choice for pages that settle after a few network connections, but pages with polling or long-lived requests may never satisfy an idle condition. In that case, choose another waitUntil condition such as domcontentloaded, and wait explicitly for the page state or selector your test needs. The measurement is only as repeatable as the navigation and interaction sequence.
2. Understand the byte calculation
Each returned entry has source text and one or more ranges recorded as used. The example sums entry.text.length for total source bytes, then adds range.end - range.start - 1 for each used range. The used-byte percentage is:
used bytes / total bytes * 100
The zero-total guard returns zero instead of producing NaN when no script or stylesheet was reported. The value is a byte-oriented approximation based on the returned source strings and ranges; it is not a percentage of features, functions, files, or the entire application.
Keep JavaScript and CSS percentages separate when diagnosing a page. The combined percentage is useful as a compact summary, but combining the two can conceal a low-coverage stylesheet or script bundle.
3. Choose the measurement window
Initial page load
Start both collectors before page.goto(), as in the runnable example. Stop after the chosen load condition and any required asynchronous rendering have completed. If you stop immediately at DOM readiness while the application continues loading modules or applying styles, that later work will be absent.
Interaction coverage
Start collection before the interactions, then exercise a deterministic sequence: open menus, change tabs, submit representative forms, or trigger the route and states you want represented. Wait for each resulting state before moving on. Coverage from a single path cannot establish that untouched paths are unused.
Multi-page journeys
Do not rely on resetOnNavigation: false to preserve JavaScript coverage across page navigations. Chrome may discard the previous page’s execution environment and its coverage data. The safer pattern is to stop before leaving each page, start new collectors on the next page, and combine the returned reports for analysis.
const allJs = [];
const allCss = [];
for (const url of ['https://example.com/', 'https://example.com/account']) {
const page = await browser.newPage();
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage(),
]);
await page.goto(url, { waitUntil: 'networkidle2' });
// Run page-specific actions here.
allJs.push(...await page.coverage.stopJSCoverage());
allCss.push(...await page.coverage.stopCSSCoverage());
await page.close();
}
const journey = summarize([...allJs, ...allCss]);
console.log(journey);
This records each page independently and merges the resulting entries afterward. If the same source appears on multiple pages, the merged byte totals count each returned entry; treat that as per-page observed coverage, not a deduplicated inventory of unique source files.
4. Configure JavaScript and CSS collection
Pass options when starting collection to change what Puppeteer reports. Check the API documentation for the version installed in your project because documented defaults can change across versions.
| Collector option | Default | Effect and when to change it |
|---|---|---|
JS resetOnNavigation |
true |
Resets JavaScript coverage on navigation. Turning it off is not a guarantee that coverage survives navigation; collect pages separately for a multi-page journey. |
JS reportAnonymousScripts |
false |
Include anonymous scripts, such as code created by eval or new Function. Generated URLs commonly use a debugger://VM form unless a sourceURL comment supplies a URL. |
JS useBlockCoverage |
true |
Collect block-level coverage. Set to false for function-level coverage. |
JS includeRawScriptCoverage |
false |
Include raw V8 script coverage data when downstream processing needs it. |
CSS resetOnNavigation |
true |
Resets CSS coverage on navigation. |
Example with non-default JavaScript options:
await page.coverage.startJSCoverage({
resetOnNavigation: true,
reportAnonymousScripts: true,
useBlockCoverage: false,
includeRawScriptCoverage: true,
});
await page.coverage.startCSSCoverage({ resetOnNavigation: true });
The four JavaScript options are documented in Puppeteer’s JSCoverageOptions and startJSCoverage() references. CSS has its own startCSSCoverage() method and options.
5. Know what the report omits
- Anonymous JavaScript scripts are omitted by default. Enable
reportAnonymousScriptswhen those scripts matter to the measurement. - Dynamically injected CSS style tags without sourceURLs are not included in CSS coverage.
- Code that runs before collection starts or after it stops is outside the measurement window.
- Unexercised routes and interactions are not evidence that their code is dead; they were simply not observed in this run.
These boundaries matter when using coverage to decide what code to remove. Treat the result as evidence from a defined run, then broaden the scenario set and review dynamic code behavior before making removal decisions.
6. Export coverage for Istanbul
Puppeteer’s Coverage documentation points to puppeteer-to-istanbul for converting coverage into a format used by Istanbul tooling. This is optional: the used-byte calculation above requires no conversion. Use the converter when your project already consumes Istanbul reports and follow the package’s own current usage instructions for your installed versions.
7. Troubleshoot common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Coverage arrays are empty | The page has no reportable scripts or stylesheets, collection started after the work, or navigation failed before content loaded. | Verify navigation and response state, start collectors before navigation, and inspect the page URL and console errors. |
Percentage is NaN |
Total reported source length is zero. | Guard the denominator as shown; inspect whether the entries arrays are empty. |
| Anonymous or eval-created JS is missing | Anonymous scripts are excluded by default. | Set reportAnonymousScripts: true; use sourceURL comments if your generated code should have a recognizable source URL. |
| Injected styles are missing | CSS coverage omits dynamically injected style tags without sourceURLs. | Add sourceURL metadata where appropriate and interpret the report as limited to styles Puppeteer returns. |
| Coverage disappears after navigation | Chrome may discard the prior execution environment, even if reset-on-navigation behavior was changed. | Stop before navigating, collect each page independently, then merge the arrays. |
| Results vary between runs | Different interactions, timing, route data, or asynchronous rendering changes which code executes. | Use stable test data, explicit waits, and a documented repeatable interaction sequence. |
| Navigation waits indefinitely | A page may keep network connections open or poll continuously, so the selected network-idle condition is never met. | Use a suitable alternative such as domcontentloaded and wait for a specific selector or application-ready condition. |
8. Performance, reliability, and cost
Coverage is collected inside a live browser page and returns source text and ranges that your script must retain and process. Keep collection windows focused on the scenario being measured, close pages and the browser in cleanup paths, and avoid collecting an unnecessarily broad journey when a smaller repeatable scenario answers the question.
For reliable comparisons, keep the Puppeteer version, page data, viewport, navigation condition, and interaction sequence consistent. Record the tested routes and actions beside the result. A byte percentage from one run is a diagnostic measurement, not a universal statement about the application.
The Puppeteer approach uses your own browser automation environment; its cost depends on the infrastructure and time you spend running that environment. The coverage API itself reports usage data and does not provide a hosted screenshot or coverage service.
Or skip the browser setup
If the task is to capture a clean screenshot rather than inspect execution coverage, ScreenshotNeo can return an image or PDF from one GET request. See the API documentation for parameters and response details.
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}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools, and the free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. These are screenshot features, not JavaScript or CSS coverage measurements.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Does Puppeteer coverage tell me whether code is safe to delete?
No. It tells you what ran in the measured scenarios. Expand the scenarios to include relevant routes, states, and user actions before treating unobserved code as a removal candidate.
Should I use block-level or function-level JavaScript coverage?
Use the default block-level collection for finer execution detail. Function-level coverage is available with useBlockCoverage: false when that granularity better fits your analysis.
Can I use coverage for a screenshot-only task?
Coverage is unnecessary if you only need a rendered image or PDF. Puppeteer can still capture pages, or use a screenshot API such as ScreenshotNeo when you want a hosted one-call capture.


