How Screenshot APIs Handle JavaScript-Heavy SPAs

2026-04-28 | Tags: [technical, screenshot-api, javascript, spa, react, vue]

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:

  1. Browser launch (or pool reuse): A headless Chromium instance starts, or is claimed from a pre-warmed pool.
  2. 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.
  3. Script execution: The browser parses and executes the JavaScript bundle. React/Vue/Angular mounts and begins rendering.
  4. Wait condition: The API waits for a signal that the page is "ready." This is the critical step.
  5. 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:

  1. Start with wait=networkidle
  2. If the screenshot shows loading state: increase wait time or add wait_for
  3. 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
  4. If fonts or images are missing: add 500ms additional wait
  5. 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).