How Screenshot APIs Handle JavaScript-Heavy SPAs
If you've tried to screenshot a React or Vue application with a simple HTTP fetch, you got a nearly blank page — maybe a loading spinner, maybe just a <div id="root"></div>. That's the SPA problem: the content lives in JavaScript, not in the initial HTML response.
Screenshot APIs solve this by running a real browser (Chromium, typically via Playwright or Puppeteer) that executes JavaScript, waits for the application to render, and then captures the result. But "runs a real browser" glosses over a lot of complexity. Here's what actually happens.
The Rendering Pipeline
When a screenshot API receives a request for a JavaScript-heavy URL, the sequence looks like this:
- Browser launch (or pool reuse): A headless Chromium instance starts, or is claimed from a pre-warmed pool.
- Navigation: The browser loads the URL. The initial HTML response arrives — for a typical SPA, this is a small shell:
<html>, a CSS link, and a script bundle reference. - Script execution: The browser parses and executes the JavaScript bundle. React/Vue/Angular mounts and begins rendering.
- Wait condition: The API waits for a signal that the page is "ready." This is the critical step.
- Screenshot: Once the wait condition is satisfied, the viewport is captured.
The wait condition is where most SPA screenshot problems originate.
Wait Strategies
Different wait strategies suit different applications:
wait=networkidle
Waits until there are no more than 2 open network connections for 500ms. This is the most reliable general-purpose wait for SPAs because it captures the moment after initial data fetches complete.
GET /api/screenshot?url=https://app.example.com&wait=networkidle
When it works: Most React/Vue/Angular apps that fetch data on mount, then render.
When it fails: Apps with persistent WebSocket connections, long-polling, or analytics that fire continuously.
wait=domcontentloaded
Fires when the initial HTML has been parsed and all blocking scripts have executed. For SPAs, this is almost always too early — React hasn't rendered the app yet.
When to use: Static sites, server-rendered pages, WordPress.
wait=load
Fires when all synchronous resources (images, fonts, scripts) have loaded. Better than domcontentloaded for SPAs, still often too early.
wait=N (fixed delay)
Waits N milliseconds after navigation completes before capturing. Simple, predictable, but wasteful: most pages render in 800ms, waiting 2000ms adds unnecessary overhead at scale.
GET /api/screenshot?url=https://app.example.com&wait=2000
When to use: When you know the rendering time is consistent and other wait strategies produce inconsistent results.
Element wait (most precise)
Wait until a specific CSS selector appears in the DOM. This is the most reliable strategy for SPAs where you know the structure:
GET /api/screenshot?url=https://app.example.com&wait_for=.main-content
The API polls for the selector and captures as soon as it resolves.
Common SPA Screenshot Problems
Problem: Loading spinner captured instead of content
Cause: The wait condition resolved while the app was still fetching data.
Fix: Use wait=networkidle to wait for data fetches to complete, or wait_for=.content-loaded if the app adds a class when ready.
Problem: Fonts not loaded — text appears in system fallback
Cause: Custom fonts (Google Fonts, self-hosted) are loaded asynchronously and may not be ready when the screenshot fires.
Fix: Add a brief additional wait, or use wait_for on an element that only renders after fonts load. Some screenshot APIs handle font loading explicitly — check the docs.
Problem: Images missing or broken
Cause: Lazy-loaded images haven't entered the viewport, or CDN images are still loading.
Fix: If you need full-page screenshots with all images, use full_page=true and wait=networkidle. Be aware that full-page screenshots with many lazy images can be slow.
Problem: Component in loading state
Cause: The component fetches its own data and hasn't finished by the time the screenshot fires.
Fix: wait_for the rendered content selector, not just a parent wrapper.
Problem: Auth wall / redirect
Cause: The app requires authentication and redirects to a login page.
Fix: Screenshot APIs can't log in to your app unless you inject session cookies or tokens. Pass them as part of the request if the API supports custom headers or cookies:
GET /api/screenshot?url=https://app.example.com&headers={"Cookie":"session=abc123"}
Or use a public preview/shareable URL instead of the authenticated version.
SPA-Specific Configuration
For a typical React SPA on hermesforge.dev:
import requests
response = requests.get(
"https://hermesforge.dev/api/screenshot",
params={
"url": "https://app.example.com/dashboard",
"format": "png",
"width": 1440,
"height": 900,
"wait": "networkidle", # Wait for data fetches
"full_page": "false", # Viewport-only (faster)
"block_ads": "true", # Block trackers that cause network activity
},
headers={"X-API-Key": "your-key"},
timeout=30,
)
The block_ads=true parameter matters more for SPAs than static sites: tracking pixels, analytics endpoints, and ad networks often fire continuously and can prevent networkidle from ever resolving. Blocking them lets the page settle.
Viewport vs. Full-Page for SPAs
Viewport screenshots capture the visible portion of the page at the configured width/height. For a 1440×900 viewport, you get exactly what a user would see above the fold. Fast, consistent, predictable.
Full-page screenshots capture the entire scrollable page. For SPAs with infinite scroll, this may be impractical (the page can extend indefinitely). For finite-length SPAs, it's useful for documentation and QA.
# Full-page SPA screenshot
params = {
"url": "https://app.example.com",
"format": "png",
"width": 1440,
"wait": "networkidle",
"full_page": "true",
}
Note: full-page screenshots of content-heavy SPAs can be 2-5MB and take 5-15 seconds. Budget accordingly.
Testing Your Wait Strategy
Before building a pipeline, test interactively:
- Start with
wait=networkidle - If the screenshot shows loading state: increase wait time or add
wait_for - If the screenshot is blank: the SPA may be rendering on client-side only after a user interaction — check if there's a static or SSR version
- If fonts or images are missing: add 500ms additional wait
- If it's consistently correct: deploy with those parameters
The screenshot API is deterministic given the same wait conditions. Once you find the parameters that produce a correct capture, they'll produce correct captures at scale.
hermesforge.dev — screenshot API with full JavaScript rendering. Free: 10/day. Starter: $4/30 days (200/day). Pro: $9 (1000/day). Business: $29 (5000/day).