How to Start JavaScript Coverage in Puppeteer
Start Puppeteer JavaScript coverage before the page activity you want to measure, then stop it and inspect the reported script ranges. This guide covers options, navigation, and common pitfalls.
Call await page.coverage.startJSCoverage() before the navigation or interaction you want to measure. Perform the page activity, then call await page.coverage.stopJSCoverage() and inspect the returned script entries and ranges. Starting early matters: coverage can miss JavaScript that ran before precise collection began.
This guide uses Puppeteer’s JavaScript API. Its examples use ES modules and a locally installed Chromium browser. Check the documentation for your pinned Puppeteer version because the live API pages may describe a different version.
1. Install Puppeteer and start coverage
In a new project, install Puppeteer:
npm install puppeteer
Save this as coverage.mjs and run it with node coverage.mjs. Puppeteer’s package downloads a compatible Chrome for Testing browser during installation.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
// Begin before navigation so initial page scripts are included.
await page.coverage.startJSCoverage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// Exercise the routes or interactions whose JavaScript should count.
// Example: await page.click('button[data-action="open-menu"]');
// Example: await page.waitForSelector('[role="menu"]');
const entries = await page.coverage.stopJSCoverage();
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;
}
}
const percentage = totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100;
console.log(`Scripts: ${entries.length}`);
console.log(`Bytes used: ${usedBytes} / ${totalBytes} (${percentage.toFixed(2)}%)`);
for (const entry of entries) {
console.log(`${entry.url}: ${entry.ranges.length} used range(s)`);
}
} finally {
await browser.close();
}
The byte calculation follows Puppeteer’s documented example. It is useful as a summary, but the percentage describes only the scripts and activity collected during this run. It is not a universal score for the application.
2. Put the start and stop calls in the right places
- Create the page and set any required cookies, headers, or emulation before starting if those setup operations should not be part of the measured interaction.
- Start JavaScript coverage before navigating when initial-load execution matters.
- Navigate and wait for the page state relevant to your measurement.
- Exercise the routes and user actions you want represented, including delayed or lazy features.
- Stop coverage before leaving the page if you need that page’s data reliably.
- Persist or analyze the returned entries before closing the browser.
startJSCoverage() resolves when collection has started. stopJSCoverage() resolves to an array of entries, each containing a script URL, script text, and coverage ranges. A range marks source positions reported as used. When you want coverage from multiple pages, collect each page separately and merge reports in your reporting layer rather than relying on coverage surviving navigation.
3. Understand the coverage options
The current options reference lists these defaults. Confirm them against the version installed in your project.
| Option | Default | Effect |
|---|---|---|
resetOnNavigation |
true |
Resets coverage data when navigation occurs. Setting it to false does not guarantee prior data survives: Chrome can discard the previous page’s JavaScript execution environment. |
reportAnonymousScripts |
false |
Includes anonymous scripts, such as code created with eval or new Function. These are reported with a debugger://VM URL unless the script supplies a //# sourceURL=... comment. |
includeRawScriptCoverage |
false |
Includes the raw script coverage information provided by the underlying protocol in entries. |
useBlockCoverage |
true |
Controls block-level versus function-level coverage collection. Keep the default when block detail is useful; choose function-level collection if that granularity better fits the report. |
Pass options when starting coverage, for example:
await page.coverage.startJSCoverage({
resetOnNavigation: true,
reportAnonymousScripts: true,
includeRawScriptCoverage: false,
useBlockCoverage: true,
});
Anonymous scripts are excluded by default. Enable reportAnonymousScripts only when dynamically created code is part of the question being measured. Adding a source URL to dynamically created code can make reports easier to identify.
4. Interpret entries and ranges carefully
The ranges and text in each entry are the basis for used-code calculations. Keep a few limitations in mind:
- Coverage follows exercised behavior. An unopened menu, unvisited route, or untriggered feature may appear unused even though real users need it.
- Collection start time matters. The Chrome DevTools Protocol warns that JavaScript executed before precise coverage is enabled may be incomplete. Start before the page activity you intend to include.
- Navigation is a boundary. Stop before navigating away and start a separate collection for the next page when you need dependable per-page data.
- Text length and offsets are a reporting detail. Use Puppeteer’s documented range calculation for its sample percentage; avoid treating a single aggregate percentage as proof that a bundle can safely be removed.
- Compare like with like. Use the same routes, interactions, waits, browser conditions, and build when comparing coverage runs.
For Istanbul-compatible output, Puppeteer’s documentation points to puppeteer-to-istanbul. Choose a reporter workflow that fits your project’s existing coverage tooling and verify its output against your pinned dependency versions.
5. Cover multiple routes and interaction states
A useful application report usually needs more than one initial page load. A practical pattern is to start and stop around each page or scenario, then retain the scenario label with its entries:
async function collectScenario(page, label, run) {
await page.coverage.startJSCoverage();
try {
await run();
const entries = await page.coverage.stopJSCoverage();
return { label, entries };
} catch (error) {
// Stop collection even when a navigation or interaction fails.
await page.coverage.stopJSCoverage().catch(() => {});
throw error;
}
}
const reports = [];
reports.push(await collectScenario(page, 'home', async () => {
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.click('[data-action="open-menu"]');
await page.waitForSelector('[role="menu"]');
}));
reports.push(await collectScenario(page, 'pricing', async () => {
await page.goto('https://example.com/pricing', { waitUntil: 'networkidle2' });
await page.click('[data-action="annual-billing"]');
}));
This pattern is illustrative: adapt selectors and waits to the site. If an action navigates, decide whether the scenario should include both sides of that navigation. For dependable attribution, split it into separate collections around each page lifecycle.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Initial scripts are missing or ranges look incomplete | Coverage started after navigation or after scripts had executed. | Start coverage before page.goto() and before the interactions being measured. |
| Entries disappear or are incomplete after changing pages | Coverage resets on navigation by default, or Chrome discarded the prior execution context. | Stop coverage before navigation, then start a new collection for the next page and merge the resulting reports. |
Code from eval or new Function is absent |
Anonymous scripts are not reported by default. | Set reportAnonymousScripts: true; add a //# sourceURL comment to generated code where possible. |
| Used percentage is unexpectedly low | The run did not exercise relevant routes, controls, or delayed features. | Add representative user actions and wait for the resulting state before stopping coverage. |
| Used percentage is unexpectedly high | The page may have executed most loaded code, or the report may combine activity from multiple actions. | Review entries and scenario boundaries; collect individual routes and states separately. |
networkidle2 never settles |
The site keeps network connections open or continually makes requests. | Use a more appropriate navigation condition such as domcontentloaded, then wait for a specific selector or application-ready signal. |
| Script URLs are hard to identify | Bundled files, inline scripts, or anonymous generated scripts have opaque names. | Keep URL and scenario metadata in the report; use source maps or source URL annotations in your analysis workflow. |
| Coverage is inconsistent across runs | Different route state, timing, cache state, browser build, or asynchronous work changed what executed. | Pin Puppeteer, stabilize test data and waits, replay the same actions, and compare equivalent builds. |
7. Puppeteer versus Chrome DevTools Coverage
Use Puppeteer when you need repeatable, scripted collection that can run in automation. Use the Chrome DevTools Coverage panel when you want to reload and explore a page manually, inspect resources interactively, or examine JavaScript and CSS together. The DevTools documentation describes unused-code inspection as a starting point; whether code can be removed depends on the application’s architecture and runtime behavior.
8. Performance, reliability, and cost
Coverage adds browser instrumentation and returns script text and range data for analysis, so include it only in runs where the data is needed. The provided documentation does not establish a universal runtime overhead figure; measure the effect in your own environment if test duration matters.
For reliability, pin Puppeteer and its browser version, collect on stable test data, use deterministic readiness conditions, and retain route or scenario labels with each report. Treat coverage as evidence about one captured execution, not a guarantee that unobserved code is dead.
Puppeteer is open-source software; this workflow has no per-screenshot or coverage API charge. The practical costs are the compute and time used by the browser and the maintenance of representative scenarios. Check the project’s package and licensing details for your deployment context.
9. Or skip the browser setup
JavaScript coverage tells you which code ran. If your adjacent task is capturing a page image, ScreenshotNeo provides a website screenshot API and MCP server for developers. One GET request returns an image or PDF; see the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card.
10. FAQ
Does JavaScript coverage include CSS?
No. This API collects JavaScript coverage. Chrome DevTools Coverage can inspect JavaScript and CSS.
Can I use the result as a dead-code removal list?
Not by itself. It only reflects code observed during the captured routes and interactions. Expand representative scenarios and validate changes with your application’s tests.
Should I start coverage before or after page.goto()?
Before, when initial page execution is part of the measurement. Starting later excludes or can incompletely report earlier execution.


