Selenium and Puppeteer both drive a real browser from code, but they were built for different jobs. Selenium is the long-standing standard for cross-browser testing, in many languages and browsers, through the W3C WebDriver protocol. Puppeteer is a Chrome-team library for automating Chrome from Node.js through Chrome's own DevTools protocol. Playwright sits between the two and comes up in the same comparison, so it is included here too.
How we measured. On 2026-11-08, on one computer (an AMD Ryzen 9 7950X3D under WSL2), against the same local test page: Selenium 4.52.0 for Python with Chromium and ChromeDriver 156, Puppeteer 25.7.0 with its Chrome 152, and — from our Playwright vs Puppeteer runs — Playwright 1.64.1 with Chromium 156. Each run: five launches, then ten rounds of load → viewport screenshot → element screenshot → full-page screenshot → PDF; medians. Selenium and Puppeteer were run back to back, twice. The machine was under heavy load from other work (load average 16–43), so differences under roughly a third are noise.
The numbers
| Median, ms | Selenium 4.52 (Chromium 156) | Puppeteer 25.7 (Chrome 152) |
|---|---|---|
| Launch | 317–760 | 412–468 |
| Load the page | 28–58 | 53–54 |
| Viewport screenshot | 49–58 | 78–86 |
| Element screenshot | 61–90 | 73–75 |
| Full-page screenshot | 121–170 (via CDP) | 150–156 |
| 26–42 | 47–50 | |
| Whole job | 291–418 | 410–422 |
The ranges overlap almost everywhere. On a busy machine, neither is meaningfully faster for single jobs; Playwright, measured separately, landed in the same range. Two things the timings do not show matter more:
- Selenium's viewport was smaller than asked. With
--window-size=1280,800, Chrome's interface took 143 px and the page saw 1280×657; Puppeteer'ssetViewportsets the page area exactly. Both are fixable (Selenium screenshots), but screenshots differ until you do. - Selenium's Chrome driver has no full-page screenshot. We used the DevTools command through
execute_cdp_cmd; Firefox's driver has one built in.
How they talk to the browser
- Selenium sends W3C WebDriver commands — HTTP requests — to a separate driver process (ChromeDriver, geckodriver, msedgedriver, safaridriver), which controls the browser. One standard protocol means one API for every browser, and browser vendors ship the drivers. Since version 4.6, Selenium Manager finds or downloads the right driver: the docs describe it as "a command-line tool implemented in Rust that provides automated driver and browser management for Selenium" (Selenium Manager). In our run it downloaded geckodriver for Firefox on its own.
- Puppeteer talks to Chrome directly over the Chrome DevTools Protocol (and to Firefox over WebDriver BiDi): no driver in between, and access to everything DevTools can do — network interception, performance traces, emulation.
- Playwright, like Puppeteer, needs no separate driver process, and drives Chromium, Firefox and WebKit.
Selenium 4 also speaks WebDriver BiDi and, for Chromium browsers, raw CDP — so the gap in low-level control has narrowed, as our execute_cdp_cmd full-page screenshot shows.
What actually differs
| Selenium | Puppeteer | Playwright | |
|---|---|---|---|
| Languages | Java, Python, C#, Ruby, JavaScript | JavaScript / TypeScript | JS/TS, Python, Java, .NET |
| Browsers | Chrome, Edge, Firefox, Safari | Chrome, Firefox | Chromium, Firefox, WebKit |
| How it connects | W3C WebDriver via a driver process (+ BiDi, CDP for Chromium) | CDP directly (+ WebDriver BiDi) | Directly, no driver process |
| Full-page screenshot | Firefox built in; Chrome via CDP | Built in | Built in |
print_page (W3C) |
page.pdf() |
page.pdf() (Chromium) |
|
| Waiting | Explicit WebDriverWait |
waitForSelector, locators |
Auto-waiting |
| Scaling out | Selenium Grid | Your own workers | Test runner sharding |
Selenium's distinguishing strengths are the ones the table cannot time: Safari through Apple's safaridriver, a decade of ecosystem around Selenium Grid — one hub that routes test sessions to many machines and browsers — and the cloud device farms built on the same WebDriver protocol, and bindings for the languages enterprise test suites are written in. A suite written against WebDriver can move between a laptop, a Grid and a cloud provider by changing one URL, which is hard to give up once a team depends on it.
Which to pick
- Selenium for cross-browser test suites — including Safari — in Java, C#, Python or Ruby, or when your company already runs a Grid or a cloud testing service built on WebDriver.
- Puppeteer for Chrome automation in Node.js — screenshots, PDFs, scraping, performance work — where direct DevTools access matters. See our Puppeteer tutorial.
- Playwright for new end-to-end test suites that need Chromium, Firefox and WebKit with auto-waiting and screenshot assertions.
Switching for speed alone is not worth it: in our runs the three were within noise of each other for the same jobs. A migration is mostly about waiting and locating elements: Selenium code waits explicitly with WebDriverWait and finds elements with find_element, Puppeteer uses waitForSelector and $, Playwright locators that wait by themselves. Screenshots and PDFs translate almost line by line; the waiting logic is where the rewrite takes time, and where flaky tests come from.
Running them in CI
All three run headless in a container on a CI runner; the practical differences are what you install. Selenium needs the browser and its driver — Selenium Manager fetches the driver, and Selenium publishes ready-made browser images for Grid. Puppeteer downloads its own tested Chrome for Testing when you install it. Playwright installs its browser builds with npx playwright install --with-deps, system libraries included. Pin the version in each case: a browser that updates itself between runs changes screenshots for reasons that have nothing to do with your code.
If you only need the screenshots
All three are libraries: you also run and maintain the browsers. When the job is screenshots or PDFs of public pages, not tests, our screenshot API does it in one HTTP request from any of these languages — full page with lazy images loaded, cookie banners removed, device presets — at 1 render per viewport shot and 2 per full page. For tests and pages behind a login, keep the library.