Playwright Advanced#
Intercept the network, reuse a saved login, and block the junk β the difference between a browser script that crawls and one that flies.
β± ~9 min read Β· ~15 min hands-on π needs: Playwright & Selenium Β· Authenticated Scraping
Once basic automation works, three techniques make it production-grade: request interception, saved authentication state, and tracing. Together they typically cut runtime by 3β5Γ and eliminate most flakiness.
Try it in 5 minutes β make it 5Γ faster#
Images, fonts, ads, and analytics are pure overhead when you only want text. Block them at the network layer:
# /// script
# requires-python = ">=3.12"
# dependencies = ["playwright>=1.40"]
# ///
"""Compare page-load time with and without blocking heavy resources.
Setup: uv run --with playwright playwright install chromium
Run: uv run fast_browser.py
"""
import time
from playwright.sync_api import sync_playwright
URL = "https://quotes.toscrape.com/js/"
BLOCK = {"image", "media", "font", "stylesheet"}
def load(block: bool) -> float:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
if block:
# Abort heavy requests before they leave the browser.
page.route(
"**/*",
lambda route: route.abort()
if route.request.resource_type in BLOCK
else route.continue_(),
)
start = time.perf_counter()
page.goto(URL, wait_until="networkidle")
page.wait_for_selector(".quote")
elapsed = time.perf_counter() - start
browser.close()
return elapsed
print(f"normal: {load(False):.2f}s")
print(f"blocking: {load(True):.2f}s")β Same data, less time. On image-heavy sites the gap is dramatic.
Capture the API the page calls#
Interception works in the other direction too β you can read responses the page receives, which hands you the hidden JSON API without opening DevTools:
page.on("response", lambda r: print(r.url) if "api" in r.url and r.ok else None)Let the page load once with this attached, note the endpoints, then drop the browser entirely and call them with httpx. Browser to discover, HTTP client to collect.
Save the login once, reuse it forever#
Logging in on every run is slow and suspicious β and impossible with MFA. Do it once, persist the session:
# One-off, run headed so you can complete any MFA by hand:
# context = browser.new_context()
# page = context.new_page(); page.goto(LOGIN_URL)
# input("Log in in the browser window, then press Enterβ¦")
# context.storage_state(path="auth.json")
# Every run after that:
context = browser.new_context(storage_state="auth.json")auth.json holds cookies and localStorage β it is your session. Never commit it; add it to .gitignore and treat it like a password. See Authenticated Scraping for when this is appropriate at all.
Debug with a trace, not print statements#
context.tracing.start(screenshots=True, snapshots=True, sources=True)
# β¦ your automation β¦
context.tracing.stop(path="trace.zip")Then open it:
uv run --with playwright playwright show-trace trace.zipYou get a timeline with a DOM snapshot at every step β for headless failures on CI, this is far quicker than guessing.
When it fails#
| Symptom | Cause | Fix |
|---|---|---|
| Blocking broke the page | Site needs its CSS/JS to render content | Don’t block stylesheet/script; block only image/media/font |
storage_state stops working | Session expired or is IP/UA-bound | Re-harvest; keep the same UA and network path |
networkidle never fires | Page polls or holds a websocket open | Wait for a specific selector instead |
| Memory grows over a long run | Contexts/pages never closed | One context per job; close in a finally |
| Fails only on CI | No display, different UA, datacenter IP | Use the trace; see Anti-bot Patterns |
Your turn (β15 min)#
- Run
fast_browser.pyand record both timings. - Attach the
page.on("response", β¦)listener to a real site and note any JSON endpoints it reveals. - Capture a
trace.zipand open it withshow-trace. - Add
image/fontblocking to your solution from Playwright & Selenium and compare.
Checklist#
- I can block heavy resource types with
page.route. - I can discover hidden APIs by listening to responses, then drop the browser.
- I can save and reuse
storage_state, and I keep it out of git. - I debug headless failures with traces, not guesswork.
- I close contexts so long runs don’t leak memory.
Go deeper#
- Playwright β network interception β routing, aborting, mocking.
- Playwright β authentication β storage state in depth.
- Playwright β trace viewer β the debugging tool worth learning.