Documentation Blog Free tools [email protected]Log in

How to use scrapy-playwright: render JavaScript inside your Scrapy spider

scrapy-playwright routes flagged Scrapy requests through a Playwright browser to render JavaScript before parsingScrapy spidermeta={playwright}plain requests skip the browserPlaywright handlerrenders JS, waitsRendered HTMLback into Scrapy parse()

scrapy-playwright is a download handler that lets a Scrapy spider render JavaScript by routing chosen requests through a Playwright browser instead of a plain HTTP fetch. You keep Scrapy's spider, pipeline and scheduling, and flag only the requests that need a real browser — so dynamic pages become scrapeable without abandoning the framework. The tradeoff is throughput: a browser per page is far heavier than an HTTP request.

Why you'd need it

Scrapy is fast and excellent, but its default downloader only sees the server-rendered HTML — content assembled in the browser by JavaScript is invisible to it. Rather than rewrite your crawler in a browser tool, scrapy-playwright bolts rendering onto Scrapy for the pages that need it, leaving the rest on the fast HTTP path. That selective approach is the whole point: you don't pay the browser cost for pages that don't need it.

Install and configure

Install the package and its browser, then register the download handlers and the asyncio reactor in your settings:

pip install scrapy-playwright
playwright install chromium
# settings.py
DOWNLOAD_HANDLERS = {
    "http":  "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

The TWISTED_REACTOR line is mandatory and the number-one setup mistake — scrapy-playwright needs the asyncio reactor, and omitting it produces a cryptic error at startup. Set it and the handlers are ready.

Requesting a page with Playwright

Rendering is opt-in per request via meta. Only requests you flag go through the browser:

import scrapy

class MySpider(scrapy.Spider):
    name = "js"
    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/products",
            meta={"playwright": True,
                  "playwright_page_methods": [
                      {"method": "wait_for_selector",
                       "args": [".product-card"]},
                  ]},
        )
    def parse(self, response):
        # response.text is now the RENDERED HTML
        for card in response.css(".product-card"):
            yield {"title": card.css(".title::text").get()}

The playwright_page_methods list runs browser actions before the page is handed back — waiting for a selector, scrolling, clicking. As with any browser scraping, waiting for the actual content (not a fixed sleep) is what makes it reliable; without the wait you parse an empty page.

Handling interactions and pagination

Beyond a single wait, real spiders need to interact — click a load-more button, fill a search box, scroll an infinite feed — and scrapy-playwright exposes the page object so you can. Passing playwright_include_page: True in the request meta hands your callback the live Playwright page, which you drive directly and then close (always close it, or you leak browser pages and eventually stall the crawl). This is how you paginate a JavaScript feed inside Scrapy: get the page, scroll or click in a loop until no new content loads, then yield the accumulated data. It is more code than a static crawl, but it keeps a genuinely interactive target inside your existing Scrapy pipeline rather than forcing a separate tool. The discipline is the same as any browser automation — wait for what you need, act, and clean up the page handle when done.

Adding a proxy

Proxies are passed to the browser context. scrapy-playwright reads them from a launch option in settings, applying to every Playwright request:

# settings.py
PLAYWRIGHT_LAUNCH_OPTIONS = {
    "proxy": {"server": "http://pr.quanticdata.io:7777",
              "username": "USER", "password": "PASS"},
}

For scraping at scale you want rotation and, on defended targets, residential IPs — a browser on a datacenter range gets flagged fast. Point the proxy at a rotating endpoint for a fresh IP per request, and use residential proxies where the target blocks hard. The same pairing of browser plus trusted IP plus human pacing that keeps any browser scraper alive applies here; we cover it in Playwright stealth.

The throughput cost — and the alternative

Here is the reality to plan around: a Playwright request uses hundreds of megabytes and seconds of CPU where a plain Scrapy request uses almost nothing. Flag every request as playwright: True and your fast Scrapy crawler becomes a slow browser farm, with concurrency limited by RAM. The discipline is to render only the pages that truly need it — inspect the target first: if the data is in the initial HTML or a JSON API call the page makes, skip Playwright entirely and let Scrapy's fast path handle it. When you do need rendering at scale but not the operational weight of a browser fleet plus proxies inside your crawler, an alternative is to keep plain Scrapy and point its requests at a scraping API that renders and returns clean HTML or Markdown server-side — your spider stays lightweight and the browser cost moves off your machine. Many teams run scrapy-playwright for the handful of genuinely interactive pages and the API for the high-volume rendered bulk. Either way, the principle holds: rendering is expensive, so spend it deliberately.

Sources & further reading

FAQ

Quick answers on how to use scrapy-playwright.

Something else? Ask us →

What does scrapy-playwright do?

It's a Scrapy download handler that routes chosen requests through a Playwright browser so JavaScript renders before parsing. You keep Scrapy's spider, pipelines and scheduling, and flag only the requests that need a real browser with meta={'playwright': True}, leaving the rest on Scrapy's fast HTTP path.

Why does scrapy-playwright fail on startup?

Almost always because the asyncio reactor isn't set. scrapy-playwright requires TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor' in settings.py alongside the DOWNLOAD_HANDLERS entries. Omitting the reactor line produces a cryptic error at startup; adding it resolves the most common setup failure.

How do I add a proxy to scrapy-playwright?

Pass it in PLAYWRIGHT_LAUNCH_OPTIONS in settings.py as a proxy object with server, username and password. It applies to every Playwright request. For scraping at scale, point it at a rotating endpoint for per-request IPs and use residential proxies on targets that block datacenter ranges.

Is scrapy-playwright slow?

Compared to plain Scrapy, yes — each Playwright request uses hundreds of MB and seconds of CPU where an HTTP request uses almost nothing, so concurrency is RAM-limited. The fix is selective use: flag only pages that genuinely need JavaScript rendering, and let Scrapy's fast path handle everything whose data is already in the HTML.

When should I use a scraping API instead of scrapy-playwright?

When you need rendering at volume but don't want to run a browser fleet and proxies inside your crawler. Keep plain Scrapy and point its requests at a scraping API that renders and returns clean HTML or Markdown server-side — the spider stays lightweight and the browser cost moves off your machine. Many pipelines use both.

Render at scale without the browser fleet in your crawler

Keep plain Scrapy and point its requests at the scraping API — JavaScript rendered server-side, clean HTML or Markdown back, residential proxies and anti-block included. Pay per success, $2 free monthly.

Related reading