Skip to main content

Playwright Web Scraping Tutorial 2026: From Zero to Production

· 6 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

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?

FeaturePlaywrightPuppeteerSelenium
Browser supportChromium, Firefox, WebKitChromium onlyAll (via drivers)
Auto-waitBuilt-inManualManual
Network interceptionFirst-classBasicLimited
Mobile emulationBuilt-inPartialVia ChromeOptions
SpeedFastFastSlow
Stealth pluginsplaywright-extrapuppeteer-extraseleniumwire

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 PlaywrightCrawler with a pool, or deploy on Apify.

Summary

Use CaseRecommended Pattern
Static pagescheerio / CSS selectors
Dynamic SPAswaitForLoadState('networkidle')
API-backed pagesResponse interception
Authenticationpage.fill() + waitForURL()
Anti-detectionStealth context + residential proxies
Production scaleApify Actor or Crawlee

FAQ

Frequently Asked Questions

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.

Common mistakes and fixes

Playwright fails with 'TimeoutError: Waiting for selector' on dynamic pages.

Replace page.waitForSelector with page.waitForLoadState('networkidle') or increase timeout via { timeout: 30000 }. For infinite scroll, scroll in a loop and wait between iterations.

Playwright is blocked immediately on target site.

Install playwright-extra with puppeteer-extra-plugin-stealth. Set a real userAgent, viewport, and avoid navigator.webdriver leaks. For severe blocking, use Apify's residential proxies.

Memory usage spikes when launching many browser contexts.

Reuse browser instances across requests with browser.newContext(). Close context after each task. Use Playwright's built-in resource caching with route.abort() for images and fonts.