How to Capture a Page’s Rendering Process with Puppeteer
Learn when to use Puppeteer traces, screenshots, video, and CDP to capture and diagnose every stage of a page render.
Use a performance trace when you need to understand rendering work, screenshots for visual checkpoints, and video for replay. Puppeteer can capture all three. A trace records scripting, style calculation, layout, painting, loading, and frame timing for inspection in Chrome DevTools. A screenshot records one visual state. A video records the sequence so you can replay what a visitor saw.
This guide shows complete workflows, synchronization techniques, Chrome DevTools Protocol fallbacks, reproducibility controls, and fixes for common failures.
Choose the right capture
| Goal | Use | Artifact | Best analysis tool |
|---|---|---|---|
| Find slow scripts, layout, paint, or frame work | page.tracing.start() |
JSON trace | Chrome DevTools Performance panel |
| Compare the page at a known checkpoint | page.screenshot() |
PNG, JPEG, or WebP | Image viewer or visual diff |
| Replay the visible loading sequence | page.record() |
MP4 stream | Video player |
| Implement frame-by-frame collection | Chrome DevTools Protocol (CDP) | Protocol events or screenshots | Your collector |
A screenshot cannot prove what happened between two frames. A video shows appearance but does not expose the browser’s internal timing. A trace is the closest match to “the rendering process” when diagnosis is the goal.
Set up Puppeteer
npm init -y
npm install puppeteer
The puppeteer package downloads a compatible Chrome during installation. Use puppeteer-core when Chrome is managed separately; provide its executable path when launching. If installation scripts are disabled, install a browser explicitly with npx puppeteer browsers install.
Capture a performance trace
Start tracing before navigation or the interaction you want to diagnose, then stop it after the page reaches an application-specific ready state.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({
width: 1440,
height: 900,
deviceScaleFactor: 1
});
await page.tracing.start({
path: 'render-trace.json',
screenshots: true
});
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
// Replace this with the signal your application owns.
await page.locator('body').wait();
await page.tracing.stop();
await browser.close();
Open render-trace.json in Chrome DevTools’ Performance panel or a Chrome timeline viewer. The screenshots: true option adds filmstrip images to the trace, which helps correlate visual changes with scripting, layout, and paint events.
Use a real readiness signal
networkidle0 means there are no active network connections at the moment Puppeteer checks. It does not mean that a framework finished rendering, an animation settled, or data was committed. Prefer a selector, attribute, application event, or explicit delay that represents readiness:
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.locator('[data-render-ready="true"]').wait();
await page.waitForFunction(() => document.fonts.status === 'loaded');
Use a bounded timeout around readiness waits so a broken application cannot leave a capture running forever.
Capture screenshots at visual checkpoints
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('[data-render-ready="true"]').wait();
await page.screenshot({
path: 'rendered.png',
fullPage: true
});
await browser.close();
Capture one element
const card = page.locator('[data-testid="hero-card"]');
await card.wait();
await card.screenshot({ path: 'hero-card.png' });
Element screenshots are preferable when the question concerns one component. Full-page screenshots are useful for page-level visual diffs, but very tall pages can consume substantial memory.
Make visual captures reproducible
- Fix viewport width, height, and
deviceScaleFactor. - Set the same browser and Puppeteer versions for every run.
- Set color scheme, locale, timezone, and media emulation explicitly.
- Use stable test data and disable random content.
- Wait for fonts, images, data, and animations to settle.
- Record cache and network conditions alongside the image.
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'light' }
]);
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
await page.emulateTimezone('UTC');
Record a video of the render
Current Puppeteer exposes page.record(), backed by Chrome’s screen-recording protocol. It writes an MP4 stream.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
const recorder = await page.record({ path: 'render.mp4' });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('[data-render-ready="true"]').wait();
await recorder.stop();
await browser.close();
Start recording before navigation to include the initial load. Start it after navigation when you only need an interaction. Stop it after the final state is reached; otherwise the recording can include idle time and become unnecessarily large.
Maintaining older screencast code
The older page.screencast() API is deprecated. Existing implementations commonly produce WebM/VP9 at 30 FPS and require ffmpeg for conversion or processing. Prefer page.record() for new MP4 workflows, and pin compatible browser versions when maintaining legacy collectors.
Use the Chrome DevTools Protocol directly
CDP is useful when you need protocol-level controls or a frame-by-frame collector.
const client = await page.createCDPSession();
await client.send('Page.enable');
// Protocol-level operations are available here:
// Page.captureScreenshot
// Page.startScreencast
// Page.startScreenRecording
With Page.startScreencast, Chrome emits screencastFrame events. Your code must acknowledge each frame with the matching session ID. Failing to acknowledge frames can stop or throttle delivery.
client.on('Page.screencastFrame', async ({ data, sessionId, metadata }) => {
// data is a base64-encoded image frame.
// Save or process it, then acknowledge it.
await client.send('Page.screencastFrameAck', { sessionId });
});
await client.send('Page.startScreencast', {
format: 'png',
everyNthFrame: 1
});
Capture navigation, interaction, and animation phases
For a useful trace or video, define the exact phase you are studying. A navigation-only capture answers different questions from a menu-open or infinite-scroll capture.
await page.tracing.start({ path: 'menu-trace.json', screenshots: true });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('[data-render-ready="true"]').wait();
await page.getByRole('button', { name: 'Open menu' }).click();
await page.locator('[data-menu-state="open"]').wait();
await page.tracing.stop();
For animations, wait for a deterministic end condition rather than guessing a delay:
await page.waitForFunction(() => {
const panel = document.querySelector('[data-panel]');
return panel?.getAttribute('data-animation') === 'complete';
});
Collect diagnostics with every artifact
Save console output, page errors, failed requests, browser versions, and capture settings beside the trace, screenshot, or video. These records explain whether a visual difference came from application code, missing resources, or the environment.
const failures = [];
const consoleMessages = [];
const pageErrors = [];
page.on('requestfailed', request => {
failures.push({ url: request.url(), error: request.failure() });
});
page.on('console', message => {
consoleMessages.push({ type: message.type(), text: message.text() });
});
page.on('pageerror', error => pageErrors.push(String(error)));
// Run the capture...
console.log(JSON.stringify({
puppeteer: (await import('puppeteer/package.json', { with: { type: 'json' } })).default.version,
failures,
consoleMessages,
pageErrors
}, null, 2));
Performance, reliability, and cost considerations
- Trace overhead: tracing adds work and can increase artifact size, especially with screenshots enabled. Trace only the interaction under investigation.
- Video overhead: recording continuously consumes CPU, disk, and encoding time. Keep the viewport and duration to the smallest useful values.
- Screenshot cost: full-page captures require layout and rasterization for the entire document. Capture an element when that answers the question.
- Synchronization: application-owned readiness signals are more reliable than a fixed sleep or network-idle heuristic.
- Repeatability: font availability, locale, timezone, browser version, cache, and third-party requests can change pixels and timings.
- Failure handling: always close the browser in a
finallyblock and preserve partial diagnostics when navigation fails.
let browser;
try {
browser = await puppeteer.launch();
// capture work
} finally {
await browser?.close();
}
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Trace file is missing or empty | tracing.stop() was not reached, or the process exited early |
Use try/finally, await both start and stop, and keep the browser alive until the file is written. |
| Capture ends before content appears | Navigation readiness was mistaken for application readiness | Wait for a known selector, data attribute, font state, or app event. |
TimeoutError while waiting |
Selector never appears or the page failed | Check URL and console errors, inspect failed requests, and set a realistic timeout. |
| Screenshot differs between runs | Unfixed viewport, fonts, locale, animations, data, or browser version | Set all rendering inputs explicitly and disable or await animations. |
| Video will not play | Legacy screencast format or missing codec support | Use page.record() for MP4, or install/configure ffmpeg for legacy WebM processing. |
| CDP frames stop arriving | Screencast frames were not acknowledged | Send Page.screencastFrameAck for every received session ID. |
| Chrome fails to launch in CI | Browser binary is unavailable or sandbox flags/environment are incompatible | Install the Puppeteer browser explicitly, verify the executable path, and use the runtime’s documented container settings. |
| Trace is too large | Long recording window or screenshots enabled for unrelated work | Narrow the capture interval and disable trace screenshots unless the filmstrip is needed. |
Or skip the browser setup
If you need a clean image or PDF rather than a diagnostic trace, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing result.
See the ScreenshotNeo API documentation for all options.
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}`);
It also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can Puppeteer show every render step?
A trace records browser events and timing in detail. It is the right diagnostic artifact, but it does not guarantee a screenshot for every visual change.
Which capture should run in CI?
Use traces for performance regressions, deterministic screenshots for visual regressions, and short videos when a human needs to review a transient interaction.
Should I use networkidle0?
Use it as one signal, then wait for an application-specific readiness condition whenever possible.
Can a trace replace a video?
No. A trace explains browser work; a video replays appearance. Keep both when you need diagnosis and communication.
When is CDP worth the extra code?
Use CDP when Puppeteer’s high-level APIs do not expose the protocol control, frame acknowledgement, or event stream your collector requires.


