ScreenshotNeo

BlogHow-to

Take a Screenshot of a Website with Puppeteer and Chrome DevTools Protocol

Capture a website viewport, full page, region, or element with Puppeteer. Learn when to use its high-level screenshot API or a Chrome DevTools Protocol session.

By the ScreenshotNeo team4 October 20269 min read

Puppeteer’s Page.screenshot() is the documented way to capture a website page. Use fullPage for a full-page image, clip for a specific rectangle, or ElementHandle.screenshot() for one DOM element. A Chrome DevTools Protocol (CDP) session is available when you need direct protocol calls or events; for ordinary screenshots, the Puppeteer screenshot API is the simpler documented route.

1. Install Puppeteer and capture a page

Install Puppeteer in a Node.js project. Puppeteer downloads a compatible bundled browser by default, which is the supported baseline. Using an installed Chrome can work, but Puppeteer does not guarantee compatibility with arbitrary installed browser versions.

npm install puppeteer

Save this as screenshot.mjs and run node screenshot.mjs. It navigates to a page, waits for the example readiness condition networkidle2, saves a PNG, and closes the browser even if navigation or capture fails.

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: 'screenshot.png' });
} finally {
  await browser.close();
}

networkidle2 is an example wait condition, not a guarantee that every application has finished rendering. A site may continue loading data or animations after navigation settles; see the readiness and troubleshooting sections below.

2. Choose the capture area

Viewport screenshot

The basic call captures the current viewport. Set the viewport before navigation if layout depends on screen size:

await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'viewport.png' });

In Puppeteer, the documented page API is commonly used with page.setViewport({ width, height }). Use the following form in regular Puppeteer scripts:

await page.setViewport({ width: 1440, height: 900 });

Full-page screenshot

Set fullPage: true to request a screenshot of the whole page rather than just the visible viewport:

await page.screenshot({ path: 'full-page.png', fullPage: true });

Very long pages can produce large images and take longer to capture. Pages with lazy-loaded content may need to be scrolled or otherwise prompted to load that content before the screenshot. The full-page option controls capture size; it does not itself establish that all dynamic content is ready.

Clip a rectangle

Use clip to capture a rectangular region. Its coordinates are relative to the page’s CSS pixel layout:

await page.screenshot({
  path: 'region.png',
  clip: { x: 40, y: 120, width: 800, height: 500 }
});

A clip is useful for a known region, but its rectangle must fit the content you intend to show. If you need a responsive component rather than fixed coordinates, selecting the element is usually less brittle.

Capture one element

Wait for the selector, then call screenshot() on its element handle. Puppeteer scrolls the element into view if needed. If the element is removed from the DOM before capture, the method throws.

const element = await page.waitForSelector('main');
if (!element) throw new Error('The main element was not found');
await element.screenshot({ path: 'main.png' });

Choose a stable selector such as an application-owned ID or semantic container. A transient class name may change between releases.

3. Configure screenshot output

The relevant Page.screenshot() options control capture scope, output destination, format, encoding, and background. The exact option defaults are version-sensitive; check Puppeteer’s current reference when upgrading.

Option What it does
path Saves the image to a file. A relative path is resolved from the current working directory. Without it, the call returns image data instead of writing a file.
fullPage Requests the full page; defaults to false.
clip Specifies a rectangular region to capture.
captureBeyondViewport Controls capture outside the viewport. The documented default depends on whether a clip is provided.
type Selects the format. PNG is the default; JPEG is also documented. The output path extension can be used to infer the type.
quality Controls lossy image quality for formats such as JPEG; it does not apply to PNG.
encoding Returns binary data by default, or a base64 string when set to base64.
omitBackground Hides the default white background so transparent output is possible.
fromSurface Uses the page surface; documented default is true.
optimizeForSpeed Requests speed-oriented capture behavior; documented default is false.

Return image data instead of saving a file

With default binary encoding, Page.screenshot() returns a Uint8Array. You can write it yourself or send it to another service. With base64 encoding, it returns a string.

import { writeFile } from 'node:fs/promises';

const imageBytes = await page.screenshot({ type: 'png' });
await writeFile('screenshot.png', imageBytes);

const imageBase64 = await page.screenshot({ encoding: 'base64' });
console.log(imageBase64.slice(0, 40));

For JPEG, choose the format explicitly and supply a quality value if desired:

await page.screenshot({
  path: 'screenshot.jpg',
  type: 'jpeg',
  quality: 80
});

Use PNG when exact edges or transparency matter. JPEG is useful when a smaller lossy image is acceptable. The documented quality setting does not apply to PNG.

Transparent background

For a transparent capture, enable omitBackground. The page’s own background styling can still paint an opaque background; remove or override that styling if transparency is required.

await page.screenshot({ path: 'transparent.png', omitBackground: true });

4. Wait for the page state you need

Navigation readiness and application readiness are separate concerns. Choose a navigation condition, then wait for content that matters to your capture. Puppeteer’s guide demonstrates networkidle2; it is a useful option for some pages, but pages with long-lived connections, polling, delayed rendering, or background requests may need a different condition.

For a specific component, wait for its selector:

await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready="true"]');
await page.screenshot({ path: 'report.png', fullPage: true });

If the site has no reliable readiness marker, a deliberate delay is possible, but it is less robust than waiting for a meaningful selector or application state:

await new Promise(resolve => setTimeout(resolve, 1500));
await page.screenshot({ path: 'after-delay.png' });

For lazy images, scroll through the document before taking a full-page capture so the page has an opportunity to load content that appears near the bottom. Site behavior differs, so verify that the image sources and other deferred content are actually present before capturing.

5. Use Chrome DevTools Protocol from Puppeteer

Puppeteer can create a CDP session attached to a page. The session supports protocol calls with send() and subscriptions to protocol events. Use it when you need direct protocol access for a task beyond Puppeteer’s high-level page API.

const client = await page.createCDPSession();
// Use client.send(method, parameters) with the relevant CDP method.
// A CDP session also supports subscriptions to protocol events.

The official API reference establishes the session creation and send() mechanism, but does not provide a complete raw Page.captureScreenshot request and response recipe. This guide therefore uses page.screenshot() for image capture instead of presenting unverified raw protocol parameters. For normal screenshots, the high-level method already covers viewport, full-page, clip, file, format, and encoding needs.

6. Runnable script: full-page capture with an element option

This complete script uses Puppeteer’s bundled browser, waits for a target page, captures a full-page PNG, and optionally captures a selected element if it exists. Save as capture.mjs.

import puppeteer from 'puppeteer';

const targetUrl = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 900 });
  await page.goto(targetUrl, {
    waitUntil: 'networkidle2',
    timeout: 60_000
  });

  await page.screenshot({
    path: 'page.png',
    fullPage: true,
    type: 'png'
  });

  const element = await page.$('main');
  if (element) {
    await element.screenshot({ path: 'main.png' });
  }
} finally {
  await browser.close();
}

Run it with node capture.mjs https://example.com. The 60-second navigation timeout is an example application choice, not a Puppeteer requirement. Set a limit appropriate to your environment and page.

7. Troubleshooting

Symptom Likely cause Fix
Screenshot is blank or missing data Capture ran before the application rendered, or content is loaded after navigation. Wait for a meaningful selector or application state after navigation. Treat networkidle2 as one possible signal, not a universal guarantee.
Element screenshot throws or element is missing The selector did not match, or the element detached from the DOM before capture. Use waitForSelector, check the handle, choose a stable selector, and ensure the page does not rerender the target during capture.
Full-page capture omits lazy content Images or sections load only when scrolled into view. Scroll through the page before capture and wait for deferred elements or images to load.
Navigation wait never completes Some pages keep network connections active or continually poll. Choose a different navigation readiness condition, then explicitly wait for the content needed in the image.
Image was returned but no file appears No path was specified. Write the returned Uint8Array to disk, or provide a path option.
JPEG quality setting has no effect quality does not apply to PNG. Select type: 'jpeg' and set quality, or keep PNG when lossless output is wanted.
Transparent PNG has a solid background The page itself paints a background color. Use omitBackground: true and ensure page CSS does not paint the area opaque.
Installed Chrome fails to launch or behaves differently The installed browser version differs from Puppeteer’s bundled supported baseline. Use the bundled browser for reproducibility, or align the installed browser version with the Puppeteer version in use.

8. Performance, reliability, and cost

  • Capture only what you need. A viewport or element image is generally a smaller task than a very long full-page capture, though this guide reports no measured performance comparison.
  • Keep browser processes bounded. Reuse a browser for a batch of pages when appropriate and close pages and browsers in cleanup paths. Each capture still depends on page load, rendering, and image size.
  • Make readiness explicit. Waiting on a meaningful selector or application state makes captures more repeatable than relying on an arbitrary sleep alone.
  • Set timeouts and handle failures. Navigation, selector waits, and capture can fail. Use try/finally to close the browser and record enough context to diagnose the failing URL and stage.
  • Budget for output size. Full-page images can be large. Use JPEG when lossy compression is acceptable, and do not request base64 unless that representation is useful to the next step.
  • Account for infrastructure. Local Puppeteer has no per-screenshot API fee, but running a browser consumes compute, memory, storage, and engineering time. No benchmark or cost estimate is asserted here.

9. Or skip the browser setup

If you need the screenshot rather than browser orchestration, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters and response behavior.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie and consent banners are accepted like a visitor; 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot. Each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
  • An MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents, including Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

10. Frequently asked questions

Does Puppeteer use Chrome DevTools Protocol?

Puppeteer can attach a CDP session to a page with page.createCDPSession(). Its high-level screenshot methods are the documented path for typical captures.

Can Puppeteer return an image without writing a file?

Yes. Omit path and the screenshot call returns image data: binary by default, or a base64 string when requested.

Can I capture just one part of a page?

Yes. Use clip for a coordinate-based rectangle or ElementHandle.screenshot() for a DOM element.

Does full-page mode guarantee every section is loaded?

No. It controls the capture area. Your page may need extra waits or scrolling to render lazy or application-loaded content.