ScreenshotNeo

BlogHow-to

How to Fix a Screenshot API Capture That Shows a Loading Spinner

A screenshot can start before an app finishes rendering. Wait for the page’s actual ready state, then capture; here are patterns for Playwright, Puppeteer, and ScreenshotNeo.

By the ScreenshotNeo team4 October 20268 min read

Fix the spinner by waiting for the page’s application content to be ready before taking the screenshot. A browser navigation event such as DOMContentLoaded or load can finish while a single-page app is still fetching data, hydrating, or rendering a chart. Wait for a page-specific success signal: the expected content becomes visible, the loading indicator becomes hidden, or a known readiness condition becomes true.

Network idle can help when a page settles cleanly, but it only describes network activity; it does not prove the content is ready. Playwright discourages using networkidle as a testing readiness criterion and recommends web assertions instead. Playwright Page API

1. Find out what the spinner is waiting for

Before changing the wait, identify what the spinner represents. It may cover an API request, client-side hydration, a chart render, an iframe, or a third-party widget. Inspect the page at the same viewport and with the same cookies, authentication, and browser context as the screenshot job. Note a stable selector for the actual result and, if available, one for the spinner.

Prefer a result selector that only appears when useful content is ready. Waiting for a generic element such as body, or for the spinner merely to exist, can succeed before the page is usable. If the app is yours, add a stable readiness signal, such as a test ID on completed content or a documented JavaScript flag.

2. Wait for application readiness in Playwright

This runnable Node.js example navigates, waits for results to appear and the spinner to disappear, then saves a screenshot. Replace the example URL and selectors with those from the target page.

import { chromium } from 'playwright';

const url = 'https://example.com/dashboard';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

try {
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
  await page.locator('[data-testid="results"]').waitFor({
    state: 'visible',
    timeout: 20000,
  });
  await page.locator('[data-testid="loading-spinner"]').waitFor({
    state: 'hidden',
    timeout: 20000,
  });
  await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
  await browser.close();
}

Install Playwright with npm install playwright and install its browser with npx playwright install chromium. If the page has no spinner selector, waiting for the result alone may be enough. If the result selector is present before its contents are populated, wait for a more specific element or a known state change.

Playwright locator waits support visibility states and timeouts. Check the API reference for the exact options supported by your installed version. Playwright Locator API

3. Wait for application readiness in Puppeteer

Puppeteer can wait for a visible result and a hidden spinner before capture. Replace the example selectors with page-specific selectors.

import puppeteer from 'puppeteer';

const url = 'https://example.com/dashboard';
const browser = await puppeteer.launch({ headless: true });

try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000 });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
  await page.waitForSelector('[data-testid="results"]', {
    visible: true,
    timeout: 20000,
  });
  await page.waitForSelector('[data-testid="loading-spinner"]', {
    hidden: true,
    timeout: 20000,
  });
  await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
  await browser.close();
}

Install it with npm install puppeteer. Puppeteer’s screenshot guide demonstrates waiting for networkidle2 at navigation, and its API also supports selector and function waits. Those are available techniques, not universal readiness recipes. Puppeteer screenshot guide · Puppeteer Page API

4. Choose a wait condition that matches the page

Wait condition Use it when Limitation
Expected result visible A stable selector appears when the useful page content is ready. A selector that appears too early can still precede complete rendering.
Spinner hidden or detached The page has a stable loading-indicator selector. Some pages hide the spinner before the result is populated; pair it with a result condition.
Known page state or readiness flag The application exposes a reliable state change. Requires a page-specific signal and, for a third-party page, a supported way to observe it.
Network idle The page’s requests settle and the automation tool’s definition fits the page. It is a network proxy, not proof of rendered readiness. Ongoing requests may prevent it; rendering may continue after it resolves.
Fixed delay A simple fallback is needed and the page’s load time is predictable. Short delays capture too early; long delays waste time. A delay never verifies success.

Playwright defines networkidle as no network connections for at least 500 ms and marks it discouraged for testing readiness. Prefer a web assertion about the actual page state when possible. Playwright Page API

Use network idle only as a fallback when it makes sense for the page. For example, a stream or recurring request may keep activity going, while an idle interval can occur before client-side rendering is done. These are consequences of using network activity as the signal, not guarantees about any particular page.

5. Configure a hosted screenshot API carefully

For a hosted API that accepts a URL instead of giving you a live browser page, consult that provider’s docs for its supported wait options. Look for a selector wait, a page function or script, network idle, or a post-navigation delay. Confirm that the wait runs before capture and that its timeout covers the page’s expected readiness time. Parameter names and defaults differ across providers, so do not assume an option such as waitForSelector or waitUntil exists.

If the service supports a selector wait, target the result or the hidden state of the spinner and set a bounded timeout. If it supports a custom readiness script, make that script return true only after the content you need is rendered. Keep the viewport, cookies, headers, and authentication consistent with the browser session where you diagnosed the page.

6. Troubleshoot waits that time out or still capture a spinner

Symptom Likely cause What to check or change
Result selector times out Selector does not match, content is inside a frame or shadow root, or the page never reached the expected state. Inspect the rendered DOM in the same browser context; confirm selector spelling and frame/shadow-root boundaries.
Spinner wait times out The spinner stays visible because a request failed, or the selector points to the wrong element. Inspect the page and network responses; verify the spinner selector and whether an error state replaced success.
Network-idle wait hangs The page keeps making requests or the service’s idle definition is not reached. Use a page-specific condition, or configure documented request blocking if appropriate and supported.
Network idle resolves but spinner remains Network quiet did not correspond to application readiness. Wait for the expected content or a known ready state instead.
Works locally, fails in the API job The job uses different cookies, authentication, viewport, user agent, or access conditions. Match the local browser context and inspect for a login, consent, or error screen.
Screenshot is blank or shows an error page The target may not have loaded successfully, or a service check may have intervened. Inspect page text, response status, and any intermediate capture the tool exposes before increasing waits.

When diagnosing, capture an intermediate screenshot or inspect page text and network responses if your tool allows it. That can distinguish a slow app from a selector mistake or an access screen. Treat these as hypotheses to check; without the provider and target page, no single cause can be assumed.

7. Keep captures reliable and efficient

  • Use semantic readiness: tie capture to the content the screenshot is meant to show.
  • Bound every wait: set a timeout and handle timeout failures explicitly so one slow page does not stall a job indefinitely.
  • Keep conditions specific: a page-specific selector is less sensitive to unrelated requests than network idle.
  • Retry selectively: if a transient load fails, retry the capture with a bounded policy; do not retry a deterministic bad selector endlessly.
  • Control the browser context: viewport, cookies, headers, and authentication can change the rendered page and its readiness path.
  • Balance speed against certainty: short, relevant waits avoid unnecessary delay; long fixed sleeps increase latency without proving that content rendered.

The cited library documentation describes wait mechanisms, not performance benchmarks for your page or screenshot provider. Measure the target workflow under its actual network and job conditions before choosing timeouts or concurrency.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Make one GET request for a screenshot; its API supports a wait-for-selector option when you need the capture to follow a page-specific signal. See the ScreenshotNeo API documentation for parameter details. This example captures a page after its results selector is ready:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/dashboard \
  --data-urlencode wait_for_selector='[data-testid="results"]' \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com/dashboard",
        "wait_for_selector": '[data-testid="results"]',
    },
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/dashboard',
  wait_for_selector: '[data-testid="results"]',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await (await import('node:fs/promises')).writeFile('shot.webp', bytes);

Use the selector that means the page is ready; a generic selector will not fix an application that has not rendered its results. ScreenshotNeo accepts and removes cookie/consent banners, newsletter popups, and chat widgets before capture, with each step configurable. Bot checks, blank pages, and failed loads are never billed. Its responses identify page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.

9. Frequently asked questions

Should I wait for load or DOMContentLoaded?

They mark document navigation milestones, not necessarily completion of client-side data fetching and rendering. Use them to start navigation, then wait for the page’s own readiness signal.

Is a longer timeout the fix?

Only if the page is genuinely slow and the condition is correct. A longer timeout cannot make a wrong selector match or make a failed page succeed.

What if I cannot change the target application?

Use a stable visible result selector or hidden spinner selector that the page already exposes. If the hosted service cannot wait on such a condition, use its documented alternatives or run browser automation where you control the page context.

Why does the page look ready in a browser but not in the capture?

The capture may use a different viewport, session, cookie state, authentication, or timing. Reproduce the screenshot job’s context while inspecting the page.