How to Read Page Performance Metrics With Puppeteer
Learn to collect Puppeteer metrics, read navigation timings, and measure Core Web Vitals without confusing browser counters with user experience.
Puppeteer’s page.metrics() reports browser runtime counters and JavaScript heap measurements. The Navigation Timing API reports milestones in a document navigation. Core Web Vitals describe user-facing loading, visual stability, and responsiveness. These measurements answer different questions; do not combine them into one generic “page speed” score.
For a repeatable check, navigate under an explicitly chosen lifecycle condition, then collect both Puppeteer’s counters and the document’s navigation entry. Treat the result as lab evidence for that browser run, not as a substitute for field data from real visitors.
1. Choose the measurement layer
| Measurement | What it answers | What it does not answer |
|---|---|---|
page.metrics() |
What runtime counters and JavaScript heap values did Puppeteer report for this page? | Whether important content appeared quickly or the page felt responsive. |
| Navigation Timing | When did key document-navigation milestones occur? | Whether the page remained visually stable or interactions were fast. |
| Core Web Vitals | How did loading, visual stability, and responsiveness affect users? | All browser internals or every cause of a regression. |
Puppeteer’s metrics include document and frame counts, JavaScript event-listener count, and JavaScript heap size values. The Metrics interface documents heap sizes in bytes. Its timestamps are monotonic seconds from an arbitrary point in the past, not wall-clock timestamps; compare durations or deltas only when the values share a meaningful timing context. See the official Puppeteer Page API and Metrics interface.
Navigation Timing describes phases of document navigation. For example, domInteractive marks when DOM construction has finished and scripts can interact with the DOM; DOMContentLoaded timestamps bracket that event handler; domComplete indicates the document and subresources are finished loading; and load-event timestamps bracket the load handler. These are navigation milestones, not a direct measurement of perceived speed. See MDN’s Navigation Timing guide.
Google lists LCP, CLS, and INP as the stable Core Web Vitals: loading, visual stability, and responsiveness, respectively. Use appropriate Web Vitals instrumentation for these questions. Google’s Web Vitals guidance recommends evaluating aggregated field data and checking that recommended thresholds are met for at least 75% of page visits.
2. Collect metrics with Puppeteer
Install Puppeteer in a Node.js project with npm install puppeteer. The following CommonJS script accepts a URL, navigates until the document’s load event, reads Puppeteer metrics, and extracts Navigation Timing values in the page context. Save as measure.cjs and run node measure.cjs https://example.com.
const puppeteer = require('puppeteer');
async function main() {
const url = process.argv[2];
if (!url) throw new Error('Usage: node measure.cjs https://example.com');
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
const response = await page.goto(url, {
waitUntil: 'load',
timeout: 60000,
});
const pptrMetrics = await page.metrics();
const navigation = await page.evaluate(() => {
const nav = performance.getEntriesByType('navigation')[0];
if (!nav) return null;
return {
startTime: nav.startTime,
domInteractive: nav.domInteractive,
domContentLoadedEventStart: nav.domContentLoadedEventStart,
domContentLoadedEventEnd: nav.domContentLoadedEventEnd,
domComplete: nav.domComplete,
loadEventStart: nav.loadEventStart,
loadEventEnd: nav.loadEventEnd,
duration: nav.duration,
transferSize: nav.transferSize,
encodedBodySize: nav.encodedBodySize,
decodedBodySize: nav.decodedBodySize,
};
});
console.log(JSON.stringify({
url,
status: response ? response.status() : null,
pptrMetrics,
navigation,
}, null, 2));
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
page.evaluate() runs a function in the page context and awaits a returned promise. The callback above reads the browser’s navigation entry rather than Node.js process timings. For the API behavior, see Puppeteer’s evaluate() reference.
A reported null response means the navigation did not produce a response object, for example due to a navigation failure. A missing navigation entry can also yield null; do not silently treat missing data as zero. A zero load-event timestamp can mean the load event had not completed when the entry was read, so collect after the lifecycle event you intend to measure.
3. Interpret the values
Puppeteer runtime counters
Use counters to investigate runtime changes across controlled runs. A larger heap value may be worth investigating, but a single reading is not proof of a leak: page state, loaded content, garbage collection, and the collection moment can affect it. Compare equivalent scenarios and record both used and total heap values where present. Counts such as frames and event listeners are context about the page, not user-experience scores.
Navigation milestones
Read each milestone as a point in the navigation sequence. If DOMContentLoaded is early but load is late, investigate work or resources that delay the load event. If both are early but the main content appears late, the application may render important content after those events. A late milestone identifies when something happened; it does not by itself identify the cause.
Navigation entry durations are in milliseconds, whereas Puppeteer’s documented timestamps are in seconds. Check each API’s units before calculating or comparing values. Keep browser-reported navigation timings separate from Node-side elapsed time unless you define how the clocks align.
Core Web Vitals and field evidence
Use LCP, CLS, and INP when asking about user-centric loading, stability, and responsiveness. A Puppeteer navigation script can provide controlled lab observations, but one load does not represent the range of devices, networks, visits, or interactions real users experience. Google notes that JavaScript measurements may differ from Chrome UX Report (CrUX) data and points to the web-vitals library as a production-ready wrapper designed to align with Google’s tools. Use field data and real-user monitoring alongside lab runs when diagnosing user experience.
4. Make runs comparable
A performance result is meaningful only with its test conditions. Record the following with each run:
- Exact URL and run timestamp, plus response status.
- Puppeteer and browser versions.
- Viewport and device emulation settings, including device scale factor where applicable.
- Navigation wait condition and timeout.
- Cache state, service-worker behavior, and whether the browser context was fresh.
- Network and CPU throttling settings, if any.
- Whether the run includes an interaction and when measurements are taken.
Apply viewport and emulation settings before navigation where practical; changing them can resize or reload a page. Puppeteer exposes viewport, device, CPU, network, cache, and service-worker controls. Chrome DevTools also documents network and CPU throttling. Its CPU throttling is relative to the host computer and does not reproduce mobile CPU architecture exactly, so describe it as a test condition rather than a perfect phone simulation. See Chrome’s Performance features reference.
Use the same conditions for baseline and comparison runs. If you want cold-cache behavior, make that explicit and use the same cache and service-worker setup for every sample. If you want warm-cache behavior, preserve that state consistently. Avoid comparing a throttled cold run with an unthrottled warm run as if only the code changed.
5. Capture a screenshot alongside a performance run
A screenshot can help identify what was visibly present at a chosen point in the navigation, but it does not replace timing data or Core Web Vitals. If you capture one with Puppeteer, use the same URL, viewport, and wait condition as the metric run, and record the capture point. Full-page screenshots can change memory use and add work, so run a separate measurement pass when the capture itself could affect the result.
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
await page.screenshot({ path: 'page.png', fullPage: true });
Or skip the browser setup
For a screenshot without installing or configuring Puppeteer, make one request to the ScreenshotNeo API. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. The request returns an image or PDF; it does not return Puppeteer performance metrics or Core Web Vitals.
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,
)
r.raise_for_status()
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 bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
Cookie 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 are not billed, and response headers say which page verdict and billing outcome applied. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. See ScreenshotNeo and the API documentation, then sign up for 1,000 free screenshots a month with no card.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
page.goto() times out |
The chosen lifecycle event did not occur before the timeout, or the page kept loading. | Check reachability and response behavior. Set a deliberate timeout and choose the lifecycle event that matches the question; record that choice. Do not interpret timeout as a performance measurement. |
| Response is null or navigation throws | The browser could not complete the navigation, or it was interrupted. | Catch and report navigation errors separately from metric output. Check the URL, browser logs, redirects, and whether another navigation began. |
| Navigation entry is missing | The page context has no navigation entry for the measured document, or the page was navigated in an unusual way. | Return a missing value, verify the page’s performance entries, and ensure collection runs in the intended document context. |
| Load timing is zero or incomplete | The relevant event had not finished when data was read. | Wait for the lifecycle milestone being measured, then read the entry; do not convert zero to a claimed fast load. |
| Two runs differ widely | Browser version, cache, service worker, viewport, throttling, server state, or page scenario changed. | Record and align conditions, take multiple runs, and investigate outliers rather than relying on one sample. |
| Good load time but poor perceived speed | Load events do not establish when the main content became visible or when interactions became responsive. | Measure appropriate Web Vitals and inspect field data; use navigation milestones only for navigation phases. |
| Heap values rise between observations | Page state or garbage collection differs; a rise alone does not prove a leak. | Repeat the same scenario, take readings at consistent points, and investigate sustained growth across comparable runs. |
Performance, reliability, and cost notes
- Keep collection lightweight. Read only the metrics needed for the question. Screenshots, tracing, and extra page activity can change the workload being measured.
- Separate test setup from page performance. Browser launch, navigation, and Node-side processing are not interchangeable with in-page navigation timings.
- Repeat and report distributions. Individual lab runs can vary. Preserve raw observations and summarize repeated runs rather than presenting one number as universal.
- Use a stable browser environment. Pin or record browser and Puppeteer versions in CI so upgrades are visible as changes in the measurement setup.
- Account for infrastructure cost. Puppeteer requires a browser process and resources to run it; parallel jobs increase resource demand. Choose concurrency and run frequency to fit the CI environment.
- Use field monitoring for user impact. Controlled runs help isolate regressions; real-user data captures actual variation. Treat them as complementary sources.
FAQ
Can page.metrics() tell me whether a page is fast?
No single counter answers that. It reports browser runtime and memory information. Use Navigation Timing for navigation phases and Web Vitals for user-centric loading, stability, and responsiveness.
Does waitUntil: 'networkidle0' mean the page is ready for users?
No. It is a network-activity condition with a defined idle interval, not proof that rendering, application work, or user interactions are complete. Choose the wait condition as part of the test definition.
Are Puppeteer lab results the same as CrUX?
No. Puppeteer measures a controlled browser run; CrUX summarizes anonymized real-user measurements. The evidence complements rather than replaces the other.
Can I use Puppeteer to measure INP?
A navigation-only collection script does not measure a representative INP by itself. INP concerns responsiveness across interactions; use suitable Web Vitals instrumentation and representative interaction or field data.


