How to Capture Beyond the Viewport with Puppeteer and Chrome DevTools Protocol
Capture full pages with Puppeteer, or use Chrome DevTools Protocol for explicit clipping and beyond-viewport control. Includes runnable code and fixes for common issues.
To capture a whole page that extends below the visible browser window, use Puppeteer’s Page.screenshot() with fullPage: true. For explicit control over a clipped region or Chrome’s protocol-level captureBeyondViewport setting, attach a Chrome DevTools Protocol (CDP) session and call Page.captureScreenshot.
A full-page screenshot captures rendered output; it does not guarantee that lazy images, infinite-scroll content, delayed widgets, or animations have finished. Prepare the page and verify that the content you need is present before capturing. Check the Puppeteer and Chrome versions you run, since protocol compatibility can vary.
1. Capture a full page with Puppeteer
For the ordinary “capture beyond the viewport” case, Puppeteer’s high-level API is the simplest route. Install Puppeteer in a Node.js project:
npm install puppeteer
Save this as full-page.mjs and run it with node full-page.mjs:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
await browser.close();
}
fullPage: true is Puppeteer’s explicit request to capture the full page. The screenshot guide uses Page.screenshot() for page captures. The networkidle2 wait condition is a useful navigation choice in some cases, but it is not a guarantee that every page’s content is ready.
Wait for content you actually need
If the target page lazy-loads images as they approach the viewport, navigate and then trigger its own loading behavior before taking the screenshot. For a simple page where scrolling is enough to request offscreen content, you can use a helper such as this, then wait for a known selector:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(async () => {
const step = Math.max(300, window.innerHeight);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
await page.waitForSelector('[data-page-ready="true"]', { timeout: 10000 });
await page.screenshot({ path: 'full-page.png', fullPage: true });
This is practical page preparation, not a Puppeteer guarantee. Replace the selector with one meaningful to your target. Infinite-scroll pages may keep adding content as you scroll, so define a stopping condition, such as a known item count or a “load more” state, instead of assuming the document height will stabilize by itself.
2. Use Chrome DevTools Protocol for explicit control
Use direct CDP when you need to state the protocol flag explicitly or request a particular rectangular clip. Puppeteer can create a CDP session attached to a page, and the session’s send() method issues protocol commands.
import puppeteer from 'puppeteer';
import { writeFile } from 'node:fs/promises';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const client = await page.createCDPSession();
const { data } = await client.send('Page.captureScreenshot', {
format: 'png',
captureBeyondViewport: true,
// Optional region, in device-independent pixels:
// clip: { x: 0, y: 0, width: 1280, height: 2400, scale: 1 },
});
await writeFile('cdp-capture.png', Buffer.from(data, 'base64'));
await client.detach();
} finally {
await browser.close();
}
The returned data is base64-encoded. The example decodes it and writes a PNG. The optional clip specifies a requested region; its coordinates are in device-independent pixels. Choose x, y, width, and height according to the crop you need. A clip is a rectangle, not an automatic instruction to discover and capture the full document height.
Clip a specific region
For a deliberate crop, set the clip explicitly and keep captureBeyondViewport: true visible in the call:
const { data } = await client.send('Page.captureScreenshot', {
format: 'png',
captureBeyondViewport: true,
clip: { x: 0, y: 1200, width: 1000, height: 700, scale: 1 },
});
The clip values describe the requested region in device-independent pixels, and scale controls the clip scale. Confirm the resulting crop against your page and Chrome version. The protocol reference does not promise successful capture for arbitrary dimensions or pathological page sizes.
3. Understand fullPage, clip, and captureBeyondViewport
| Option | What it means | When to use it |
|---|---|---|
fullPage |
Puppeteer’s high-level full-page screenshot request. It defaults to false. |
Use true for the conventional complete-page screenshot. |
clip |
A rectangular capture region. It does not itself mean “capture the whole document.” | Use when you need a specific crop or region. |
captureBeyondViewport |
A lower-level setting that controls whether capture can extend beyond the viewport. | Set explicitly in raw CDP calls, especially when supplying a clip. |
Puppeteer documents captureBeyondViewport as false by default when there is no clip and true otherwise. The CDP protocol reference documents its default as false. These are related controls, not interchangeable ones: fullPage is Puppeteer’s higher-level request, clip selects a rectangle, and captureBeyondViewport is the protocol-level beyond-viewport switch. If you call CDP directly, write the flag explicitly so the requested behavior is clear.
4. Choose the capture API for the job
| Approach | Best for | Controls | Trade-off |
|---|---|---|---|
Puppeteer Page.screenshot() |
Conventional full-page or page screenshots | fullPage, clip, path and image options |
Less protocol plumbing; Puppeteer owns the higher-level call. |
CDP Page.captureScreenshot |
Explicit protocol settings or a custom clip | captureBeyondViewport, clip, format, quality, surface and speed options |
More control, with closer dependence on Chrome and its protocol. |
Puppeteer ElementHandle.screenshot() |
A particular DOM element | Element handle and screenshot options | Captures an element, not the full document. |
Puppeteer’s element screenshot scrolls the element into view if needed and then uses the page screenshot mechanism. For example:
const card = await page.$('.pricing-card');
if (!card) throw new Error('Could not find .pricing-card');
await card.screenshot({ path: 'pricing-card.png' });
Use an element screenshot when the desired artifact is that element. It is not a substitute for a full-document capture.
5. CDP format and capture options
The protocol’s Page.captureScreenshot method accepts these relevant controls:
| Parameter | Use | Notes |
|---|---|---|
format |
Choose png, jpeg, or webp. |
The protocol reference lists these formats. |
quality |
Set lossy image quality. | Relevant to JPEG and WebP; the protocol defines the parameter, but check the Chrome version you deploy for accepted behavior. |
clip |
Request a rectangular region. | Coordinates are device-independent pixels; set a region that matches the intended crop. |
captureBeyondViewport |
Allow the requested capture beyond the viewport. | CDP documents a default of false; set it explicitly when needed. |
fromSurface |
Choose the capture source surface. | Protocol-level control; use only when your capture case needs it and verify against the target Chrome version. |
optimizeForSpeed |
Request the protocol’s speed-oriented capture option. | It is an option, not a published performance guarantee. |
For normal full-page work, prefer Puppeteer’s wrapper. Reach for direct CDP when a protocol-specific setting is needed. The Puppeteer documentation consulted identifies version 25.12.0, while CDP’s tot reference moves over time. Check the documentation for the Puppeteer package and Chrome release you actually deploy.
6. Practical limits, reliability, and performance
- Large documents: Do not assume every page can be captured as one unlimited-height image. Very large documents can create substantial image data and memory pressure. If the capture fails or becomes unwieldy, capture meaningful sections or test a tiled approach against the target page.
- Lazy content: A screenshot records rendered output; it does not establish that all offscreen resources were requested. Scroll or use the page’s own loading controls, then verify target elements and images.
- Infinite scroll: Define how much content is required and when to stop. Scrolling can add more content indefinitely.
- Changing pages: A page that updates during capture can produce visually inconsistent output. If consistency matters, consider pausing animations or waiting for the application’s stable state, then verify the result. These are page-specific techniques, not guarantees from the screenshot API.
- Format and data size: PNG is lossless and can create larger files; JPEG and WebP offer lossy quality controls in CDP. Select a format appropriate for the image and downstream use.
- Browser compatibility: Direct CDP is Chrome-specific here. For another browser or a different Chrome release, verify that the relevant protocol method and options are supported.
There are no performance percentages or universal maximum dimensions established by the documentation cited here. Measure against representative pages in the same browser environment you plan to run in production.
7. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Only the visible viewport appears | The call did not request full-page capture, or the capture used a viewport-sized clip. | For Puppeteer, set fullPage: true. For CDP, check the clip and set captureBeyondViewport: true explicitly. |
| The screenshot is missing offscreen images or sections | Those resources or sections had not loaded when capture began. | Trigger the page’s lazy-loading behavior, wait for a relevant selector or state, and verify the content before capture. |
| The clip is shifted or the crop is wrong | The clip coordinates or dimensions do not match the intended region, or the wrong coordinate assumptions were used. | Review x, y, width, height, and scale; CDP clip coordinates are in device-independent pixels. |
| CDP rejects the command or option | The Chrome build or protocol version may differ from the reference you followed. | Check the deployed Chrome protocol documentation and supported parameters. Try Puppeteer’s high-level screenshot API if you do not need the low-level option. |
| The output cannot be opened as an image | CDP’s returned data is base64 text and may have been written without decoding. |
Decode it with Buffer.from(data, 'base64') before writing the file. |
| Capture timing is inconsistent | networkidle2 may occur before application-specific rendering is complete, or a page may keep changing. |
Wait for an application-specific selector or readiness signal. Treat navigation wait conditions as hints, not proof of visual readiness. |
| The process hangs or consumes too much memory | The page may be unusually large, still loading, or producing a large image. | Set appropriate navigation and selector timeouts, capture a smaller region, reduce output dimensions where your chosen API supports it, or split the work into sections. |
8. Or skip the browser setup
If you want a screenshot without managing a browser, ScreenshotNeo takes a URL in one API request and returns an image or PDF. The ScreenshotNeo website describes its screenshot API and MCP server for developers. See the API documentation for request options.
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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, newsletter 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. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
9. FAQ
Does fullPage make lazy-loaded images load?
It requests a full-page screenshot; it is not a guarantee that the page has loaded every offscreen asset. Prepare the page and check the content before capture.
Should I use Puppeteer or CDP?
Use Puppeteer’s screenshot API for a standard full-page capture. Use CDP when you need explicit protocol-level clip or capture controls.
Can an element screenshot capture the whole document?
No. An element screenshot targets a particular DOM element. Use the page screenshot API for a full-page capture.
Where can I check the option names and defaults?
Use the official Puppeteer ScreenshotOptions reference, the Puppeteer screenshot guide, and the Chrome DevTools Protocol Page.captureScreenshot reference. The protocol’s tot documentation changes; verify the behavior for your deployed versions.


