JS Rendering
crw supports JavaScript rendering for single-page applications (SPAs) and JS-heavy sites via the Chrome DevTools Protocol (CDP).
Rendering Modes
Set the rendering mode in your config:
[renderer]
mode = "auto" # auto | lightpanda | playwright | chrome | none
page_timeout_ms = 30000
pool_size = 4
| Mode | Behavior |
|---|---|
auto |
Use HTTP first, detect SPAs, fall back to CDP if needed |
lightpanda |
Always use LightPanda for JS rendering |
playwright |
Always use Playwright for JS rendering |
chrome |
Always use Chrome for JS rendering |
none |
HTTP only, never render JS |
Per-request control
Override the rendering mode per request using renderJs:
{
"url": "https://spa-app.com",
"renderJs": true,
"waitFor": 3000
}
| Value | Behavior |
|---|---|
null (default) |
Auto-detect based on heuristics |
true |
Force CDP rendering |
false |
HTTP only |
When To Turn It On
Enable JS rendering when the page content is not present in the initial HTML response. Typical examples:
- single-page applications,
- pages that fetch content after hydration,
- and sites where the meaningful body is assembled client-side.
Do not enable it blindly for every request. HTTP-only fetches are faster and cheaper.
Choosing a waitFor Value
Start with the smallest value that works:
500to1000for lightly hydrated pages,2000for typical JS-heavy pages,3000to5000only when you have confirmed the target hydrates slowly.
:::warning Long waits are not automatically safer. They increase latency and can hide the fact that the page is blocked rather than merely slow. :::
Auto-detection
When renderJs is null, crw fetches the page via HTTP first, then checks for SPA signals:
Triggered when body text < 200 chars and:
- Contains
id="root",id="app",id="__next",id="__nuxt",id="__gatsby",id="svelte" - Contains
ng-app,data-reactroot - Contains
window.__initial_state__,__next_data__,window.__remixcontext,window.__astro
Triggered when:
<noscript>contains "enable javascript"- Body text < 500 chars and URL matches Framer, Webflow, Wix, or Squarespace domains
CDP Backends
LightPanda (Recommended)
Fastest option. Lightweight browser engine purpose-built for scraping.
# Auto-install
crw-server setup
# Manual start
lightpanda serve --host 127.0.0.1 --port 9222 &
[renderer.lightpanda]
ws_url = "ws://127.0.0.1:9222/"
Playwright
[renderer.playwright]
ws_url = "ws://playwright:9222"
Chrome / Chromium
[renderer.chrome]
ws_url = "ws://chrome:9222"
How CDP rendering works
- Open a new browser tab via
Target.createTarget - Navigate to the URL
- Attach to the target via
Target.attachToTarget - Wait for the configured time (
waitForor default 2000ms) - Execute
Runtime.evaluate("document.documentElement.outerHTML")to get rendered HTML - Close the tab via
Target.closeTarget
Cloud vs Self-hosted
- Cloud: JS rendering is always available. The managed infrastructure runs a LightPanda sidecar alongside the engine. Available on fastcrw.com (cloud).
- Self-hosted: You must run
crw-server setupor configure a CDP browser (LightPanda, Chrome, or Playwright) in yourconfig.tomlunder[renderer]. If no JS renderer is configured, requests withrenderJs: truewill fall back to HTTP-only fetching and include a warning.
What To Inspect in the Response
When rendered output looks wrong, check:
metadata.renderedWithto verify a browser was actually used,metadata.elapsedMsto understand the cost of the request,- and
warningto catch anti-bot or fallback situations.
Troubleshooting
- Empty content from JS-heavy sites: Increase
waitFor(e.g.,3000-5000). Some SPAs need extra time to hydrate. renderedWith: "http_only_fallback"in metadata: JS rendering was requested but no renderer is available. Check your deployment configuration.- Internal error on
renderJs: true: Verify the LightPanda sidecar is running and reachable. Check/healthfor renderer status. - Still poor output after increasing
waitFor: The issue may be anti-bot protection or authentication flow, not rendering delay.
Docker Compose
The included docker-compose.yml runs crw with a LightPanda sidecar:
services:
crw:
image: ghcr.io/us/crw:latest
ports:
- "3000:3000"
environment:
- CRW_RENDERER__LIGHTPANDA__WS_URL=ws://lightpanda:9222
lightpanda:
image: lightpanda/lightpanda:latest
command: ["serve", "--host", "0.0.0.0", "--port", "9222"]