Puppeteer vs Selenium for Screenshots: Measured

Puppeteer vs Selenium on the same Chrome build: cold and warm capture times, full-page and element screenshots, the viewport trap, and which to choose.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot, a screenshot API that runs headless Chrome for you. I use Puppeteer daily and Selenium less often. The numbers below come from both tools running on my machine against the same browser build, and the scripts are included.

Short answer: for screenshots, pick by language and browser coverage, not by speed. Driving the same Chrome build, Puppeteer and Selenium captured pages in nearly the same time. Puppeteer gives you full-page capture, request blocking and network-idle waits with one option each. Selenium gives you Python, Java, C#, Ruby and JavaScript bindings, real Safari and Firefox through WebDriver, and Grid for spreading work across machines, but it needs DevTools commands for some screenshot jobs, and it has one sizing trap that silently changes your output.

This comparison sits with our other browser automation and screenshot tool comparisons. If Playwright is also on your list, read Playwright vs Selenium next.

At a glance

Puppeteer Selenium
Languages (official) JavaScript / TypeScript (Node.js) Java, Python, C#, Ruby, JavaScript
Browsers Chrome / Chromium, Firefox Chrome, Edge, Firefox, Safari (and others with a driver)
Protocol Chrome DevTools Protocol, WebDriver BiDi W3C WebDriver (classic), WebDriver BiDi
Viewport screenshot page.screenshot() save_screenshot() / takeScreenshot()
Full-page screenshot fullPage: true Firefox: built-in. Chrome: a DevTools command
Element screenshot elementHandle.screenshot() element.screenshot()
Output formats PNG, JPEG, WebP PNG
Wait for network idle waitUntil: 'networkidle0' Not built in; wait for an element or condition
Block requests (ads, trackers) setRequestInterception BiDi or DevTools network interception
Distributed runs Your own queue Selenium Grid
Driver setup Downloads a matching Chrome on install Selenium Manager resolves a driver at run time

Test setup

Tested on 2026-10-15 with Node 22.18.0, [email protected], [email protected] and Python selenium 4.50.0, on macOS with an Apple M1 Pro.

  • Same browser for both: Chrome for Testing 154.0.8037.57 in headless mode. Selenium used chromedriver 154.0.8037.57, pinned by path so that Selenium Manager's network lookup was not part of the timing.
  • Targets: https://example.com over the network, and two local pages served from 127.0.0.1: a documentation page and a dashboard with an SVG chart.
  • Cold: start the browser, load the page, take one viewport PNG, quit. 5 runs per target.
  • Warm: one browser, then 10 loads and captures in a row. Full-page: 5 captures in the same warm browser.
  • Wait condition: the load event for both tools, so the comparison is fair.

Results

Target Tool Cold median (p95) Warm median (p95) Full page warm median
example.com Puppeteer 971 ms (1,120) 133 ms (211) 118 ms
example.com Selenium 833 ms (1,688) 151 ms (381) 146 ms
docs page Puppeteer 664 ms (741) 83 ms (101) 63 ms
docs page Selenium 683 ms (985) 63 ms (129) 77 ms
dashboard Puppeteer 614 ms (640) 84 ms (120) 83 ms
dashboard Selenium 699 ms (743) 68 ms (132) 67 ms

With 5 to 10 runs per cell, differences of a few dozen milliseconds are noise. An earlier Puppeteer pass in the same session gave a 752 ms cold median and 52 ms warm median on the dashboard, so the spread between passes was as large as the spread between tools. Two conclusions hold up:

  1. Neither tool is meaningfully faster. Both send a few protocol messages to the same browser. The browser's start-up and the page's own load time dominate.
  2. Cold starts cost 0.6 to 1 second; warm captures cost under 0.2 seconds. If you start a browser per screenshot, you pay that start-up every time. Reuse the browser, whichever library you pick. Our guide to scaling Puppeteer screenshots covers pooling.

The viewport trap in Selenium

The byte counts in our run did not match between tools, so we checked the image sizes. With --headless=new --window-size=1280,800, Selenium's screenshot was 1280x657, not 1280x800. The window size includes space the headless browser keeps for its UI, so 143 px of height were missing from every viewport capture. Puppeteer's setViewport() sets the page viewport directly, so its output was 1280x800.

This matters for anything with a fixed size: social cards, visual diffs, thumbnails. The fix is to measure the inner size and grow the window by the difference:

inner_w, inner_h = driver.execute_script("return [innerWidth, innerHeight]")
rect = driver.get_window_rect()
driver.set_window_rect(width=rect["width"] + 1280 - inner_w,
                       height=rect["height"] + 800 - inner_h)

After this, our Selenium screenshot was 1280x800.

Full-page screenshots: the bigger code difference

Puppeteer has one option for it. With Selenium and Chrome you ask the DevTools protocol directly, because the Chrome driver has no full-page method. (Firefox's driver does, and Python exposes it as save_full_page_screenshot.)

Puppeteer, complete script:

// Viewport, element and full-page screenshots with Puppeteer.
import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 }); // this IS the viewport
  await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  await page.waitForSelector('body p', { timeout: 10_000 });

  await page.screenshot({ path: 'viewport.png' });
  const el = await page.$('body p');
  await el.screenshot({ path: 'element.png' });
  await page.screenshot({ path: 'full.png', fullPage: true });
  console.log('saved viewport.png, element.png, full.png');
} finally {
  await browser.close();
}

Run with npm i [email protected] and node shot.mjs. Errors propagate, so a failed load exits with a non-zero code.

Selenium (Python), complete script:

"""Viewport, element and full-page screenshots with Selenium 4 and headless Chrome."""
import base64
import os
import sys

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--hide-scrollbars")
if os.environ.get("CHROME_BIN"):
    options.binary_location = os.environ["CHROME_BIN"]
# Leave CHROMEDRIVER unset to let Selenium Manager find a driver.
service = Service(os.environ.get("CHROMEDRIVER")) if os.environ.get("CHROMEDRIVER") else Service()

driver = webdriver.Chrome(options=options, service=service)
try:
    driver.set_page_load_timeout(30)
    # --window-size sets the outer window; size the viewport instead.
    inner = driver.execute_script("return [innerWidth, innerHeight]")
    rect = driver.get_window_rect()
    driver.set_window_rect(width=rect["width"] + 1280 - inner[0], height=rect["height"] + 800 - inner[1])

    driver.get(url)
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, "body p")))
    driver.save_screenshot("viewport.png")

    driver.find_element(By.CSS_SELECTOR, "body p").screenshot("element.png")

    # Chrome full page: ask DevTools to capture beyond the viewport.
    metrics = driver.execute_cdp_cmd("Page.getLayoutMetrics", {})
    size = metrics["cssContentSize"]
    shot = driver.execute_cdp_cmd("Page.captureScreenshot", {
        "format": "png",
        "captureBeyondViewport": True,
        "clip": {"x": 0, "y": 0, "width": size["width"], "height": size["height"], "scale": 1},
    })
    with open("full.png", "wb") as f:
        f.write(base64.b64decode(shot["data"]))
    print("saved viewport.png, element.png, full.png")
finally:
    driver.quit()

Run with pip install selenium==4.50.0 and python shot.py. Both scripts produced a 1280x800 viewport PNG, a 416x77 element PNG and a full-page PNG of example.com in our test. The two scripts do the same work; the Selenium one is about twice as long, and most of the extra lines are the viewport fix and the DevTools call.

Setup and operations differences we hit

Driver resolution. Puppeteer downloads a Chrome build that matches its version when you install it. Selenium 4 resolves a driver when the script starts, through Selenium Manager. In our sandboxed network, Selenium Manager could not reach the Chrome for Testing version index, waited about 4 seconds, and then fell back to a cached chromedriver. On a fresh CI runner with no cache and no outbound access, that becomes a failure. Pin the driver path (as the script above allows) or pre-install it in your image.

API churn. Puppeteer moves fast. In Puppeteer 25, puppeteer.executablePath() returns a Promise, so code that passed it straight into another tool broke with "Browser was not found at the configured executablePath ([object Promise])". Older code that calls page.waitForTimeout() fails on v22 and later because the method was removed. Selenium's WebDriver API changes slowly, which is one reason test suites stay on it for years.

Waiting. Puppeteer's waitUntil: 'networkidle0' and 'networkidle2' handle many pages that load data after the load event. Selenium has no network-idle wait in the classic API, so you wait for a specific element, which is more reliable anyway when you know the page. For pages you do not know, Puppeteer's built-in waits save code. Our guide to waiting for single-page apps before capture compares the strategies.

Blocking ads and trackers. Puppeteer: page.setRequestInterception(true) and abort matching requests. Selenium: possible with BiDi network interception or DevTools commands, with more setup and binding-specific APIs.

Formats. Selenium returns PNG only. Puppeteer writes PNG, JPEG and WebP directly. If file size matters, see our measured PNG vs JPEG vs WebP comparison.

Decision table

Your situation Choose Why
Node.js service that makes screenshots or PDFs Puppeteer Full page, formats, waits and blocking are one option each.
Python, Java, C# or Ruby codebase Selenium (or Playwright) Official bindings; no Node runtime to add.
You must capture in real Safari Selenium WebDriver drives Safari through safaridriver. Puppeteer does not.
You already run a Selenium test suite and want failure screenshots Selenium Add save_screenshot() to your teardown.
You want to spread captures across many machines today Selenium Grid Built for it. With Puppeteer you build your own queue.
Pixel-exact social cards or thumbnails Puppeteer Direct viewport control; no window-size correction.
You do not want to run browsers at all A screenshot API See below.

Choose Selenium without hesitation when the language or the browser decides it. Choose Puppeteer when you are in Node and screenshots are the product, not a side effect of testing.

The same three captures as API calls

Both scripts above still leave you with Chrome to install, update, size and restart. If screenshots are the goal, the same three captures are three requests to RenderScreenshot, from any language:

BASE="https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com"
AUTH="Authorization: Bearer $RS_API_KEY"

curl -o viewport.png -H "$AUTH" "$BASE&width=1280&height=800"
curl -o element.png  -H "$AUTH" "$BASE&selector=body%20p"
curl -o full.png     -H "$AUTH" "$BASE&full_page=true"

In Python, with the Python SDK:

import os
from renderscreenshot import Client, TakeOptions

with Client(os.environ["RS_API_KEY"]) as client:
    full = client.take(TakeOptions.url("https://example.com").full_page())
    with open("full.png", "wb") as f:
        f.write(full)

The viewport is set directly, so there is no window-size correction, and lazy content can be scrolled into view with the capture options. Get a free API key and run the curl lines above against your own pages to compare the output with your current script.

Last tested: 2026-10-15. Re-test planned on the next major Puppeteer or Selenium release.

Frequently asked questions

Is Puppeteer faster than Selenium for screenshots?
Not in a way that matters when both drive the same Chrome. In our test, warm captures took 63 to 151 ms with either tool and cold starts took 0.6 to 1 second. The browser, not the library, sets the speed.
Why is my Selenium screenshot shorter than the window size I set?
--window-size sets the outer window, and headless Chrome still reserves space for browser UI. With --window-size=1280,800 we got a 1280x657 screenshot. Resize the window based on window.innerHeight to get the viewport you want.
How do I take a full-page screenshot in Selenium with Chrome?
Chrome's driver has no full-page method. Call the DevTools command Page.captureScreenshot with captureBeyondViewport and a clip that covers the content size. Firefox has a built-in full-page method.
Can I use Puppeteer from Python or Java?
Puppeteer is a Node.js library. From Python or Java, use Selenium or Playwright, which have official bindings for those languages.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.