Playwright vs Selenium for Screenshots: Measured

Playwright vs Selenium for screenshots: capture times, memory, default sizes and waits measured on the same pages, with tested Python and Node.js code.

By Simon Chiu · Updated

Disclosure: we build RenderScreenshot, a screenshot API that runs the browser for you. We do not make Playwright or Selenium. Both are free, and the numbers below come from running both on the same machine against the same pages; the scripts are on this page.

Short answer: for screenshots, Playwright needs less code and, with its default binary, captures faster and starts faster. Selenium wins when your team works in Ruby or C#, already runs Selenium Grid, or must test in real Safari through safaridriver. Speed alone is not a good reason to switch an existing Selenium suite: drive the same Chrome build and the gap per capture is small.

This page is part of our tool comparisons. For Puppeteer against Selenium on the same Chrome build, including the full captureBeyondViewport walkthrough, see Puppeteer vs Selenium.

At a glance

Playwright Selenium
Protocol Its own protocol over CDP (Chromium) and patched Firefox/WebKit builds W3C WebDriver through a driver process (chromedriver, geckodriver, safaridriver)
Official languages JavaScript/TypeScript, Python, Java, .NET Java, Python, C#, Ruby, JavaScript
Browsers Bundled Chromium, Firefox, WebKit builds Installed Chrome, Edge, Firefox, Safari
Full-page screenshot fullPage: true everywhere Firefox only; Chrome needs a DevTools command
Element screenshot locator.screenshot(), waits for visibility element.screenshot(), wait first yourself
Viewport Set on the context, exact Window size, minus browser UI in headless Chrome
Waiting Auto-waiting locators Explicit WebDriverWait conditions
Freeze animations, mask elements Built in Inject CSS
Scale out Many contexts per browser, test-runner workers Selenium Grid across machines

Test setup

Tested on 2026-10-15 on macOS, Apple M1 Pro, 32 GB, Node 22.18.0.

  • Playwright 1.64.1, in two modes: its default chrome-headless-shell (Chromium 156.0.8078.4), and channel: 'chromium', which runs full Chromium in new headless mode.
  • Selenium WebDriver 4.51.0 for Node, Chrome 155.0.8059.40 with chromedriver 155.0.8059.79 (resolved by Selenium Manager), --headless=new.
  • Viewport 1280 x 800 in both. For Selenium we set it with the DevTools command Emulation.setDeviceMetricsOverride, so both tools captured exactly 1280 x 800.
  • Pages: example.com; a local page with a 2,000-row table; a local single-page app that renders its content 1,200 ms after load. Each tool waited for the same #ready element on the SPA.
  • 20 warm captures per page with one long-lived browser after one warm-up capture; 10 cold runs (launch, capture example.com, quit). Memory is the peak summed RSS of all child processes during the warm runs.

Results

Setup example.com p50 / p95 2,000-row table p50 / p95 SPA p50 Cold start p50 / p95 Peak RSS
Playwright, default (headless shell) 82 / 166 ms 61 / 89 ms 1,318 ms 490 / 534 ms 452 MB
Playwright, channel: 'chromium' 113 / 208 ms 91 / 97 ms 1,324 ms 642 / 2,765 ms 1,184 MB
Selenium + Chrome, --headless=new 143 / 225 ms 100 / 109 ms 1,291 ms 883 / 981 ms 1,264 MB

What the numbers say:

  1. Most of Playwright's lead comes from the binary. The headless shell is a lighter build. When Playwright ran full Chromium too, its per-capture times moved close to Selenium's (91 vs 100 ms on the table page).
  2. Cold start favors Playwright. Selenium starts chromedriver and then Chrome, and every command crosses that extra process. Playwright's median cold run was 240 to 390 ms shorter. The one 2.8 s outlier in Playwright's full-Chromium cold runs shows why p95 needs more runs than we did.
  3. The SPA row is the page, not the tool. All three spent most of their time waiting for the app's 1,200 ms delay.
  4. Memory follows the binary. Full Chrome under either tool used more than twice the RSS of the headless shell. Summed RSS double-counts shared memory, so compare the rows; do not size servers from them.
  5. Bytes were identical. Both tools produced the same PNG sizes for the same page and viewport (for example 49,136 bytes for the table page). Selenium returns screenshots as base64 over the WebDriver connection, which adds a third to the transfer, but the decoded file is the same.

An earlier run with 15 captures per page gave the same ranking. Network variance affects the example.com column; the two local pages are the cleaner comparison.

Same capture, both tools (Python)

Python is where most teams choose between these two, so the main examples are Python. Both scripts ran against example.com and against a long local page, where the full-page captures came out identical: 1280 x 56,102 px.

Selenium 4.51:

"""Selenium 4 (Python): viewport-exact and full-page screenshots in headless Chrome."""
import base64
import sys

from selenium import webdriver
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")
driver = webdriver.Chrome(options=options)  # Selenium Manager finds or downloads chromedriver
try:
    # --window-size sets the outer window; set the viewport through CDP instead.
    driver.execute_cdp_cmd("Emulation.setDeviceMetricsOverride",
                           {"width": 1280, "height": 800, "deviceScaleFactor": 1, "mobile": False})
    driver.get(URL)  # returns when document.readyState is "complete"
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "p")))
    driver.save_screenshot("selenium-viewport.png")

    # Chrome has no full-page method in Selenium; ask CDP 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("selenium-full.png", "wb") as f:
        f.write(base64.b64decode(shot["data"]))
    print("ok", size["width"], size["height"])
finally:
    driver.quit()

Playwright 1.64:

"""Playwright (Python, sync API): viewport and full-page screenshots."""
import sys

from playwright.sync_api import sync_playwright

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

with sync_playwright() as p:
    browser = p.chromium.launch()
    try:
        context = browser.new_context(viewport={"width": 1280, "height": 800}, device_scale_factor=1)
        page = context.new_page()
        page.goto(URL, wait_until="load")
        page.locator("p").first.wait_for()
        page.screenshot(path="playwright-viewport.png")
        page.screenshot(path="playwright-full.png", full_page=True)
        print("ok")
    finally:
        browser.close()

The Selenium version is twice as long for the same two images, and the extra lines are exactly the two places where screenshot work differs: setting an exact viewport, and capturing beyond it. execute_cdp_cmd works only with Chromium-based drivers. For Firefox, use driver.get_full_page_screenshot_as_file(); Safari has no full-page method.

Default sizes: the silent difference

Run both tools with no size settings and you get different images. We captured the same element (a card with padding: 24px inside a page with 40 px margins) with each tool's defaults:

Element screenshot width
Playwright, default context (1280 x 720 viewport) 1,200 px
Selenium, default headless Chrome window 676 px

With --window-size=1280,800, Selenium's headless Chrome reported innerHeight 657, and the viewport screenshot was 1280 x 657: the window size includes space for browser UI even in headless mode. The Puppeteer vs Selenium page shows a window-resize workaround; the CDP override above is the other fix. In Playwright the viewport you pass is the viewport you get.

Waiting before the capture

Element screenshots show the design difference most clearly.

# Selenium: wait explicitly, then screenshot the element
card = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "ready")))
card.screenshot("selenium-card.png")

# Playwright: the locator waits for visibility on its own
page.locator("#ready").screenshot(path="playwright-card.png", timeout=10_000)

Both lines worked on our SPA page, where the element appears 1.2 s after load. Without the WebDriverWait, Selenium raises NoSuchElementException straight away. Playwright's locator also waits until the element stops moving, which avoids half-finished CSS transitions in the image.

Wait for Selenium Playwright
Document loaded driver.get() returns at readyState == "complete" (page load strategy normal) goto(url, wait_until="load")
DOM only page_load_strategy = "eager" wait_until="domcontentloaded"
Network quiet No built-in condition wait_until="networkidle" (discouraged in its docs)
Element visible EC.visibility_of_element_located locator.wait_for() or any locator action
Custom JS condition WebDriverWait(...).until(lambda d: d.execute_script(...)) page.wait_for_function("...")

For pages that never go quiet, a selector that appears only after the data loads is the best signal in both tools. See waiting for single-page apps.

The same in Node.js

Node teams have both options too. The Selenium driver below is the one we benchmarked.

import { Builder, By, until } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome.js';
import { writeFile } from 'node:fs/promises';

const options = new chrome.Options().addArguments('--headless=new', '--hide-scrollbars');
const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
try {
  await driver.sendDevToolsCommand('Emulation.setDeviceMetricsOverride',
    { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
  await driver.get('https://example.com');
  await driver.wait(until.elementLocated(By.css('p')), 10_000);
  await writeFile('selenium.png', Buffer.from(await driver.takeScreenshot(), 'base64'));
} finally {
  await driver.quit();
}
import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
  const page = await context.newPage();
  await page.goto('https://example.com', { waitUntil: 'load' });
  await page.screenshot({ path: 'playwright.png' });
} finally {
  await browser.close();
}

Decision table

Your situation Choose
New screenshot service in Python, Node, Java or .NET Playwright
Ruby application (Rails, Sidekiq jobs) Selenium, or a Ruby CDP library
Existing Selenium suite that already captures screenshots on failure Stay with Selenium; add the CDP full-page call where needed
Need real Safari, not WebKit Selenium with safaridriver on macOS
Many viewport or device variants per URL Playwright contexts
Captures spread across many machines today Selenium Grid, or Playwright on a job queue
Visual regression tests with stable images Playwright (animations: 'disabled', mask, toHaveScreenshot)
Lowest cold start in serverless jobs Playwright with the headless shell

Or skip the browser

Both tools leave the same operations work when screenshots run in production: browser installs and updates, fonts on Linux, memory limits, zombie processes and retries. If screenshots are a feature and not your product, a hosted renderer removes that layer. The two captures above as one request:

curl -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "viewport": { "width": 1280, "height": 800 },
    "capture": { "mode": "full_page" },
    "wait": { "for_selector": "p" }
  }' \
  --output example.png

Parameters are in viewport, capture and wait. You can run the same three pages through it with a free key and compare the output with your local captures.

Frequently asked questions

Is Playwright faster than Selenium for screenshots?
Per capture, only when Playwright uses its default chrome-headless-shell binary. Against full headless Chrome, the two were within about 10 to 30 ms per capture in our test. Cold start (launch, capture, quit) was faster with Playwright in both modes we measured.
Why are my Selenium and Playwright screenshots different sizes?
Their defaults differ. Playwright opens a 1280x720 viewport. Headless Chrome under Selenium uses its own default window, and --window-size sets the outer window, not the viewport. Set the viewport explicitly in both.
Which one should I use from Ruby or C#?
Selenium has official Ruby and C# bindings. Playwright has official .NET bindings but no official Ruby package, so Ruby teams usually pick Selenium or a Ruby CDP library.
Can Selenium take a full-page screenshot in Chrome?
Not with a built-in method. Send the DevTools command Page.captureScreenshot with captureBeyondViewport. Firefox's driver has a full-page method. Playwright has fullPage in every browser it supports.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.