Playwright vs Puppeteer vs Selenium for web scraping
Updated 1 Oct 2026 · Tested on 1 Oct 2026
For scraping, the library matters less than you might think: on four JavaScript-heavy tasks, Playwright, Puppeteer and Selenium all got the data every time, within half a second of each other. Choose by fit: Playwright for built-in waiting and three browser engines, Puppeteer for Chrome in Node.js, Selenium for the most languages and Safari.
The result
We gave each library four tasks that defeat a plain HTTP request, on two sites built for scraping practice, and ran each one three times. This is the script and what it printed:
// Four JavaScript-heavy tasks, each run 3 times with each library: did it get the data, and how long did it take?
// npm install playwright puppeteer selenium-webdriver
// timeout: 900
import { chromium } from "playwright";
import puppeteer from "puppeteer";
import { Builder, By, until } from "selenium-webdriver";
import chrome from "selenium-webdriver/chrome.js";
const TASKS = [
{ name: "Rendered by JavaScript", url: "https://quotes.toscrape.com/js/", want: 10 },
{ name: "Rendered after 10 s", url: "https://quotes.toscrape.com/js-delayed/", want: 10 },
{ name: "Infinite scroll (30+ items)", url: "https://quotes.toscrape.com/scroll", want: 30, scroll: true },
{ name: "Table loaded by a click", url: "https://www.scrapethissite.com/pages/ajax-javascript/", want: 6, click: "a#\\32 015", rows: "tr.film" },
];
// Each library does each task its own idiomatic way and returns how many items it got.
const LIBS = {
Playwright: {
async open() { const b = await chromium.launch(); return { b, page: await b.newPage() }; },
async run({ page }, t) {
await page.goto(t.url);
if (t.click) { await page.click(t.click); await page.locator(t.rows).first().waitFor({ timeout: 20_000 }); return page.locator(t.rows).count(); }
await page.locator("div.quote").first().waitFor({ timeout: 20_000 });
while (t.scroll && (await page.locator("div.quote").count()) < t.want) { await page.mouse.wheel(0, 4000); await page.waitForTimeout(300); }
return page.locator("div.quote").count();
},
close: ({ b }) => b.close(),
},
Puppeteer: {
async open() { const b = await puppeteer.launch(); return { b, page: await b.newPage() }; },
async run({ page }, t) {
await page.goto(t.url);
if (t.click) { await page.click(t.click); await page.waitForSelector(t.rows, { timeout: 20_000 }); return (await page.$$(t.rows)).length; }
await page.waitForSelector("div.quote", { timeout: 20_000 });
while (t.scroll && (await page.$$("div.quote")).length < t.want) { await page.mouse.wheel({ deltaY: 4000 }); await new Promise((r) => setTimeout(r, 300)); }
return (await page.$$("div.quote")).length;
},
close: ({ b }) => b.close(),
},
Selenium: {
async open() { return { d: await new Builder().forBrowser("chrome").setChromeOptions(new chrome.Options().addArguments("--headless=new")).build() }; },
async run({ d }, t) {
await d.get(t.url);
if (t.click) { await d.findElement(By.css(t.click)).click(); await d.wait(until.elementLocated(By.css(t.rows)), 20_000); return (await d.findElements(By.css(t.rows))).length; }
await d.wait(until.elementLocated(By.css("div.quote")), 20_000);
while (t.scroll && (await d.findElements(By.css("div.quote"))).length < t.want) { await d.executeScript("window.scrollBy(0, 4000)"); await d.sleep(300); }
return (await d.findElements(By.css("div.quote"))).length;
},
close: ({ d }) => d.quit(),
},
};
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
const rows = [];
for (const [lib, L] of Object.entries(LIBS)) {
const launches = [];
for (const t of TASKS) {
const times = [];
let ok = 0;
for (let i = 0; i < 3; i++) {
let t0 = performance.now();
const s = await L.open();
launches.push(performance.now() - t0);
t0 = performance.now();
try {
if ((await L.run(s, t)) >= t.want) ok++;
} catch {}
times.push(performance.now() - t0);
await L.close(s);
}
rows.push([lib, t.name, `${ok}/3`, (median(times) / 1000).toFixed(1)]);
}
rows.push([lib, "Browser start (median)", "", (median(launches) / 1000).toFixed(2)]);
}
console.log("library task got the data median seconds");
for (const [lib, task, ok, s] of rows) console.log(`${lib.padEnd(12)}${task.padEnd(30)}${ok.padStart(12)}${s.padStart(16)}`);library task got the data median seconds
Playwright Rendered by JavaScript 3/3 2.1
Playwright Rendered after 10 s 3/3 12.0
Playwright Infinite scroll (30+ items) 3/3 2.5
Playwright Table loaded by a click 3/3 4.5
Playwright Browser start (median) 0.11
Puppeteer Rendered by JavaScript 3/3 2.1
Puppeteer Rendered after 10 s 3/3 11.9
Puppeteer Infinite scroll (30+ items) 3/3 2.5
Puppeteer Table loaded by a click 3/3 4.0
Puppeteer Browser start (median) 0.40
Selenium Rendered by JavaScript 3/3 2.3
Selenium Rendered after 10 s 3/3 12.1
Selenium Infinite scroll (30+ items) 3/3 2.8
Selenium Table loaded by a click 3/3 4.1
Selenium Browser start (median) 0.37All three got the data every time, and their task times were within half a second of each other[1 Oct 2026]. On these pages the waiting is the page's own (the second task deliberately renders after 10 seconds), not the library's. The clearest measured difference was starting the browser: 0.11 seconds for Playwright, which uses a lightweight headless build, against about 0.4 for the other two. That matters when you start a browser per page, and not when you reuse one.
The tasks were picked to need a browser: a page built by JavaScript, the same page rendering after a delay, an infinite-scroll list that loads more as you scroll, and a table that appears only after a click. Real sites add bot protection, heavier pages and slower servers, which change the times for all three alike.
The same task in each library
Here is one task, reading the quotes from the page that renders after 10 seconds, written the way each library's documentation suggests. Each ran as shown.
Playwright
// Playwright: a page whose quotes appear 10 seconds after it loads. Locators wait for elements on their own.
// npm install playwright npx playwright install chromium
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://quotes.toscrape.com/js-delayed/");
const quotes = page.locator("div.quote");
await quotes.first().waitFor({ timeout: 20_000 });
const rows = await quotes.evaluateAll((els) => els.map((e) => ({ text: e.querySelector(".text").textContent, author: e.querySelector(".author").textContent })));
await browser.close();
console.log(`${rows.length} quotes, first by ${rows[0].author}`);10 quotes, first by Albert EinsteinLocators wait for their element automatically, and evaluateAll reads every match inside the page in one call.
Puppeteer
// Puppeteer: the same delayed page. Wait for the selector explicitly, then read the elements in the page.
// npm install puppeteer (downloads a matching Chrome)
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://quotes.toscrape.com/js-delayed/");
await page.waitForSelector("div.quote", { timeout: 20_000 });
const rows = await page.$$eval("div.quote", (els) => els.map((e) => ({ text: e.querySelector(".text").textContent, author: e.querySelector(".author").textContent })));
await browser.close();
console.log(`${rows.length} quotes, first by ${rows[0].author}`);10 quotes, first by Albert EinsteinYou wait for the selector yourself, then $$eval runs a function over every match inside the page. The API is close to Playwright's, whose authors had worked on Puppeteer before.
Selenium
// Selenium WebDriver for Node.js: the same delayed page. An explicit wait holds until the quotes exist.
// npm install selenium-webdriver (Selenium Manager downloads the matching ChromeDriver)
import { Builder, By, until } from "selenium-webdriver";
import chrome from "selenium-webdriver/chrome.js";
const driver = await new Builder().forBrowser("chrome").setChromeOptions(new chrome.Options().addArguments("--headless=new")).build();
try {
await driver.get("https://quotes.toscrape.com/js-delayed/");
await driver.wait(until.elementLocated(By.css("div.quote")), 20_000);
const rows = [];
for (const q of await driver.findElements(By.css("div.quote"))) {
rows.push({ text: await q.findElement(By.css(".text")).getText(), author: await q.findElement(By.css(".author")).getText() });
}
console.log(`${rows.length} quotes, first by ${rows[0].author}`);
} finally {
await driver.quit();
}10 quotes, first by Albert EinsteinAn explicit wait holds until the element exists. Reading each element is a separate call to the browser, which adds up on pages with thousands of items; returning the data from a single executeScript call avoids that.
How they differ
| Playwright | Puppeteer | Selenium | |
|---|---|---|---|
| Languages (official) | JavaScript/TypeScript, Python, Java, .NET | JavaScript/TypeScript (Node.js) | Java, Python, C#, Ruby, JavaScript |
| Browsers | Chromium, Firefox, WebKit; branded Chrome and Edge | Chrome, Firefox | Chrome, Edge, Firefox, Safari |
| Waiting | Automatic on locators | Explicit waitForSelector | Explicit WebDriverWait |
| Browser setup | npx playwright install downloads its browsers | Downloads Chrome for Testing on install | Uses your installed browser; Selenium Manager fetches the driver |
| Browser start in our runs | 0.11 s | 0.40 s | 0.37 s |
Language and browser support come from each project's documentation, read on 1 October 2026[1 Oct 2026].
Which to choose
- Playwright if you are starting fresh: automatic waiting means less code that breaks on slow pages, one API covers three browser engines, and it is available in Python as well as Node.js. The Python guide uses it.
- Puppeteer if your project is Node.js and only needs Chrome: it is a focused library with a large body of examples. See Web scraping with JavaScript.
- Selenium if your team works in Java, C# or Ruby, needs real Safari or Edge, or already has Selenium infrastructure such as a Grid. See Web scraping with Selenium and Python.
When none of them is the answer
All three put a browser on your machine or your server, and the work that follows is yours: keeping browsers updated, running them in parallel, and handling sites that block automated browsers or show different content by country. If that is most of your effort, a hosted service runs the browser per request. Skryp's live browser does it from an agent or the API, and its scrape call decides on its own whether a page needs a browser at all.
Questions
- Playwright or Puppeteer?
- Playwright, for most new scraping projects: locators wait automatically, it drives Chromium, Firefox and WebKit, and it works from Python, Java and .NET as well as Node.js. Puppeteer suits a Node.js project that only needs Chrome. In our runs both got the data every time at the same speed.
- Is Playwright better than Selenium for web scraping?
- For new projects it usually means less code, because it waits for elements on its own, and its browser starts faster (0.11 s against 0.37 s in our runs). Selenium is the better fit if you work in C# or Ruby, need real Safari, or already run Selenium Grid. Both got the data from every page we tried.
- Puppeteer or Selenium for web scraping?
- Puppeteer if you work in Node.js and only need Chrome; Selenium if you need another language or browser, or already have Selenium infrastructure. On our four JavaScript-heavy tasks both succeeded every time, within half a second of each other.
Evidence
On four JavaScript-heavy tasks (a rendered page, a page that renders after 10 seconds, infinite scroll and a table loaded by a click), each run three times on 1 October 2026, Playwright 1.63, Puppeteer 24.43 and Selenium 4.50 for Node.js all got the data 12 of 12 times; their median task times were within half a second of each other, and median browser start was 0.11 s (Playwright), 0.37 s (Selenium) and 0.40 s (Puppeteer).
observed test · checked 1 Oct 2026 · quotes.toscrape.com and scrapethissite.com practice pages; headless Chromium-based browsers on a MacBook in South Africa.
Limits: Practice sites, one machine, one connection; real sites with bot protection or heavy pages will differ. Playwright's start time uses its lightweight headless shell.
Selenium's main project ships language bindings for Java, Python, C#, Ruby and JavaScript and drives Chrome, Edge, Firefox and Safari; Playwright is available for JavaScript/TypeScript, Python, Java and .NET and drives Chromium, Firefox and WebKit plus branded Chrome and Edge; Puppeteer is a Node.js library that drives Chrome and Firefox.
provider docs · checked 1 Oct 2026 · selenium.dev/downloads and /documentation/webdriver/browsers, playwright.dev/docs/languages and /docs/browsers, pptr.dev/supported-browsers
Limits: Community bindings exist for other languages; only each project's official ones are listed.