Playwright Web Scraping Tutorial 2026: From Zero to Production
Playwright is the dominant headless browser for web scraping in 2026 — faster than Selenium, more reliable than Puppeteer, and with native support for Chromium, Firefox, and WebKit. This tutorial takes you from install to a production-ready scraper in under an hour.
Freshness note: Examples updated for Playwright v1.58.x (current as of March 2026). Check the official changelog for the latest release notes.
Why Playwright for Web Scraping?
| Feature | Playwright | Puppeteer | Selenium |
|---|---|---|---|
| Browser support | Chromium, Firefox, WebKit | Chromium only | All (via drivers) |
| Auto-wait | Built-in | Manual | Manual |
| Network interception | First-class | Basic | Limited |
| Mobile emulation | Built-in | Partial | Via ChromeOptions |
| Speed | Fast | Fast | Slow |
| Stealth plugins | playwright-extra | puppeteer-extra | seleniumwire |
Best for: JavaScript-heavy sites, SPAs, dynamic content, authenticated scraping.
Install Playwright
Node.js
npm init -y
npm install playwright
npx playwright install chromium
Python
pip install playwright
playwright install chromium
Scrape a Static Page (Node.js)
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/');
const quotes = await page.$$eval('.quote', (elements) =>
elements.map((el) => ({
text: el.querySelector('.text').innerText,
author: el.querySelector('.author').innerText,
}))
);
console.log(quotes);
await browser.close();
Scrape a Dynamic Page (Wait for Network)
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Wait for network idle to ensure JS has fully rendered
await page.goto('https://books.toscrape.com/', {
waitUntil: 'networkidle',
});
const books = await page.$$eval('.product_pod', (items) =>
items.map((item) => ({
title: item.querySelector('h3 a').getAttribute('title'),
price: item.querySelector('.price_color').innerText,
rating: item.querySelector('.star-rating').className.split(' ')[1],
}))
);
console.log(JSON.stringify(books, null, 2));
await browser.close();
Handle Pagination
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/');
const allQuotes = [];
while (true) {
const quotes = await page.$$eval('.quote', (els) =>
els.map((el) => ({
text: el.querySelector('.text').innerText,
author: el.querySelector('.author').innerText,
}))
);
allQuotes.push(...quotes);
const nextBtn = await page.$('li.next a');
if (!nextBtn) break;
await nextBtn.click();
await page.waitForLoadState('networkidle');
}
console.log(`Scraped ${allQuotes.length} quotes`);
await browser.close();
Intercept Network Requests (API-first Approach)
Many modern SPAs load data via XHR/fetch. Intercept the API call directly for faster, more reliable extraction:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Listen for API responses that contain the data we want
const apiData = [];
page.on('response', async (response) => {
if (response.url().includes('/api/products')) {
const json = await response.json();
apiData.push(...json.items);
}
});
await page.goto('https://example-spa.com/products');
await page.waitForLoadState('networkidle');
console.log(apiData);
await browser.close();
This pattern bypasses rendering entirely and gives you clean JSON. It's 10–100× faster than DOM scraping on API-backed pages.
Handle Authentication (Login Form)
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com/login');
await page.fill('#email', process.env.EMAIL);
await page.fill('#password', process.env.PASSWORD);
await page.click('[type=submit]');
await page.waitForURL('**/dashboard');
// Now scrape authenticated pages
await page.goto('https://example.com/dashboard/data');
const data = await page.$$eval('.row', (rows) =>
rows.map((r) => r.innerText)
);
console.log(data);
await browser.close();
Basic Stealth (Avoid Easy Detection)
import { chromium } from 'playwright';
const browser = await chromium.launch({
headless: true,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
],
});
const context = await browser.newContext({
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
viewport: { width: 1280, height: 720 },
locale: 'en-US',
timezoneId: 'America/New_York',
});
const page = await context.newPage();
// Remove the navigator.webdriver leak
await page.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
await page.goto('https://target.com');
For sites with aggressive fingerprinting (Cloudflare Bot Management, Akamai, PerimeterX), add residential proxies:
const context = await browser.newContext({
proxy: {
server: 'https://gate.iproyal.com:7777',
username: process.env.IPROYAL_USER,
password: process.env.IPROYAL_PASS,
},
});
IPRoyal residential proxies rotate IPs per request and support country, state, and city geo-targeting. Pricing starts around $7/GB for small volumes and scales down significantly at higher volumes — with non-expiring bandwidth.
Python Async Playwright
import asyncio
from playwright.async_api import async_playwright
async def scrape():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto("https://quotes.toscrape.com/")
quotes = await page.eval_on_selector_all(
".quote",
"""elements => elements.map(el => ({
text: el.querySelector(".text").innerText,
author: el.querySelector(".author").innerText,
}))"""
)
print(quotes)
await browser.close()
asyncio.run(scrape())
Scale with Apify
Playwright scrapers become complex to manage at scale: concurrent sessions, proxy rotation, retries, storage, scheduling. Apify handles all of this with a cloud-native Actor runtime.
Wrap your existing Playwright code in an Actor:
import { Actor } from 'apify';
import { chromium } from 'playwright';
await Actor.init();
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/');
const quotes = await page.$$eval('.quote', (els) =>
els.map((el) => ({
text: el.querySelector('.text').innerText,
author: el.querySelector('.author').innerText,
}))
);
await Actor.pushData(quotes);
await browser.close();
await Actor.exit();
Push to the Apify Store or run via the API. Apify's free tier includes $5 in platform credits (roughly 50 hours of Playwright runtime).
When Not to Use Playwright
- Static HTML sites: Use
fetch+cheerio(10–50× faster, far cheaper). - API-backed SPAs: Intercept network requests as shown above instead.
- Scale >100 concurrent sessions: Playwright processes are heavy. Use Crawlee's
PlaywrightCrawlerwith a pool, or deploy on Apify.
Summary
| Use Case | Recommended Pattern |
|---|---|
| Static pages | cheerio / CSS selectors |
| Dynamic SPAs | waitForLoadState('networkidle') |
| API-backed pages | Response interception |
| Authentication | page.fill() + waitForURL() |
| Anti-detection | Stealth context + residential proxies |
| Production scale | Apify Actor or Crawlee |
FAQ
Yes, in most cases. Playwright is faster, more reliable with modern SPAs, and has a better async API than Selenium. It supports all major browsers (Chromium, Firefox, WebKit) out of the box, handles auto-waiting natively, and is actively maintained by Microsoft. Selenium is only preferable if you need legacy browser support or your team has existing Selenium infrastructure.
Playwright can render any website that runs in a real browser. However, many sites use anti-bot protection (Cloudflare, DataDome) that detects automation. For these, you need additional layers: playwright-extra-stealth plugin, residential proxies, or a managed unblocking service. Playwright alone is detected by modern WAFs.
Chromium is the best choice for most scraping — it represents the majority of real user traffic and has the best DevTools protocol support. Use Firefox if a site specifically blocks Chrome-based scrapers. Use WebKit (Safari engine) for Apple-specific geo-testing.
Block unnecessary resources using route.abort() for images, fonts, and analytics scripts. Use 'domcontentloaded' instead of 'networkidle' when you do not need full page render. Share browser contexts between requests. For large-scale jobs, use Crawlee's PlaywrightCrawler which manages a concurrent worker pool automatically.
Yes, but use it only when JavaScript rendering is required — static pages are 10–50× faster with Cheerio. For production scale (hundreds of concurrent sessions), deploy on Apify or use Crawlee's PlaywrightCrawler with a configured concurrency limit.
