ScreenshotNeo

BlogHow-to

Fix Website Screenshots That Show a Loading Spinner Instead of the Page

If a website screenshot shows a loading spinner instead of the page, wait for meaningful content—not just navigation—to appear before capturing.

By the ScreenshotNeo team4 October 20269 min read

If a website screenshot shows a loading spinner instead of the page, the capture probably happened after navigation began or completed but before the application displayed its useful content. Wait for a page-specific signal—such as the expected heading becoming visible or the spinner disappearing—then take the screenshot. Use Chrome DevTools’ Network panel to see what was still happening when the spinner appeared. Network idle can help on some sites, but it is not a universal definition of readiness.

1. Find out what the browser was doing

First determine whether the page was still waiting on a request or whether the application remained in its loading state after requests completed. Chrome DevTools can show both the visual sequence and associated network activity.

  1. Open the page in Chrome and open DevTools.
  2. Select Network, open Network settings, and enable screenshots.
  3. Reload the page while the Network panel is recording.
  4. Inspect the captured frames and find one that shows the spinner. Correlate its time with requests that were active, stalled, or failed.
  5. Compare those requests with the page’s visible state. A request that remains active may explain a wait; a failed request may explain why the application never leaves its loading state.

Chrome documents this workflow for inspecting frames during loading and correlating them with network activity in its Network panel tutorial and Network reference. The timeline helps locate the problem; it does not identify a particular site’s root cause without that site’s URL and capture trace.

2. Wait for the page’s meaningful state

A navigation event and a finished-content state are different signals. A browser can reach a navigation milestone while a client-rendered application is still fetching data or displaying its loading shell. Choose a condition that represents the content you want in the image.

Readiness signal What it tells you When it fits
domcontentloaded The initial document has been parsed. When the needed content is already in the document and does not depend on later application work.
load The page load event has fired. For pages where the load event corresponds closely to the content you need.
Expected content visible A page-specific element or result has appeared. Usually the clearest choice for client-rendered or data-driven pages.
Loading element hidden A known spinner or loading indicator is no longer visible. When the application reliably removes that indicator only after the useful state is ready.
Network idle Network activity meets the automation tool’s idle condition. When the site’s request pattern makes network quiet a useful proxy for readiness.

Prefer a positive assertion about the expected content where possible. A spinner can disappear because of an error, and network quiet can occur while the application is still rendering or in a broken state. Conversely, analytics, polling, or persistent connections can keep network activity going after the useful content is already visible. Playwright lists navigation milestones including commit, domcontentloaded, load, and networkidle; it explicitly discourages using networkidle as a general test readiness signal and recommends web assertions instead. See the Playwright Page documentation.

3. Capture after a page-specific condition with Playwright

This runnable Node.js example waits for a results heading before capturing. Replace the URL and selector with a stable element that appears only when the target page is useful. Install Playwright with npm install playwright and install its browser with npx playwright install chromium.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

  try {
    await page.goto('https://example.com/search?q=widgets', {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });

    // Replace this with a stable selector for the actual useful content.
    await page.locator('h1.results-title').waitFor({
      state: 'visible',
      timeout: 20_000,
    });

    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

The navigation wait gets the document to an initial milestone; the locator wait is the readiness check for this application. Set timeouts to match the site and your job budget. A timeout should surface as a failed capture that you can diagnose, rather than silently saving a spinner as though it were a successful result.

Wait for the spinner to disappear

If the application has a stable loading selector and hides it only when content is ready, wait for it to become hidden. Confirm that the selector actually identifies the loading indicator and that an error state does not also remove it.

await page.goto('https://example.com/dashboard', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});

await page.locator('[data-testid="loading-spinner"]').waitFor({
  state: 'hidden',
  timeout: 20_000,
});

await page.locator('[data-testid="dashboard-content"]').waitFor({
  state: 'visible',
  timeout: 5_000,
});

await page.screenshot({ path: 'dashboard.png', fullPage: true });

Checking for the content after the spinner is hidden guards against a failure path where the spinner vanishes but the expected page never appears.

Use network idle only when it matches the site

Playwright supports a networkidle navigation condition, but its documentation discourages treating it as a general readiness signal. If the target page settles its requests before rendering the content and has no persistent activity, it can be a useful additional wait; keep a page-specific assertion when you can.

await page.goto('https://example.com/report', {
  waitUntil: 'networkidle',
  timeout: 30_000,
});

await page.locator('[data-testid="report-title"]').waitFor({
  state: 'visible',
  timeout: 10_000,
});

await page.screenshot({ path: 'report.png', fullPage: true });

4. Capture after navigation with Puppeteer

Puppeteer’s screenshot guide demonstrates navigating and then taking a screenshot, including an example using networkidle2. For a page that renders content asynchronously, add an explicit selector wait so the capture condition matches the page. Install Puppeteer with npm install puppeteer.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000 });

  try {
    await page.goto('https://example.com/search?q=widgets', {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });

    await page.waitForSelector('h1.results-title', {
      visible: true,
      timeout: 20_000,
    });

    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

When a site becomes ready once its network activity settles, Puppeteer also provides page.waitForNetworkIdle(). Its API reference documents the wait; use it only where the site’s behavior makes network quiet a sensible condition. See the Puppeteer screenshot guide for screenshot options.

5. Run the same readiness check in Python

Playwright’s Python API supports the same approach. Install it with pip install playwright, then install Chromium with playwright install chromium.

from pathlib import Path
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(viewport={"width": 1440, "height": 1000})
    try:
        page.goto(
            "https://example.com/search?q=widgets",
            wait_until="domcontentloaded",
            timeout=30_000,
        )
        page.locator("h1.results-title").wait_for(
            state="visible",
            timeout=20_000,
        )
        page.screenshot(path="page.png", full_page=True)
    finally:
        browser.close()

Use a selector grounded in the page’s actual markup. A broad selector such as body can be visible before the content has loaded and therefore recreate the original problem.

6. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. It accepts one GET request with a URL and returns an image or PDF. Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed, with the page verdict and billing status reported in response headers. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

For this loading-state problem, ScreenshotNeo provides a direct capture call; when the target app needs a page-specific readiness condition, use the DIY browser code above to express that condition. The API also supports wait-for-selector, delay, and network-idle options. See the ScreenshotNeo API documentation for the available parameters.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);

The API supports full-page capture with lazy images loaded, CSS selector element capture, dark mode, device presets and custom viewports, retina scale, custom CSS and JavaScript, clicking an element before capture, hiding selectors, waiting for a selector, a delay or network idle, request and resource blocking, custom headers and cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI spec. Parameters used by other screenshot APIs also work, which makes switching easier.

Plans include 1,000 screenshots per month free with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Start with 1,000 free screenshots a month, no card required.

7. Troubleshooting a spinner screenshot

Symptom Likely cause What to check or change
The screenshot is captured immediately with a spinner. The script waited for navigation but not application content. Add a visible-content or hidden-spinner wait after navigation.
A network-idle wait times out. The page keeps requests active through polling, analytics, streaming, or another persistent connection. Use a page-specific content assertion instead of requiring global network quiet.
The wait passes but the screenshot still shows the spinner. The selector matches the wrong element, a hidden duplicate, or a shell that appears before the actual content. Inspect the DOM and wait for a distinctive result, heading, or content container; confirm the intended selector is visible.
The spinner disappears, but the page is blank or shows an error. The app may remove its spinner on an error path, or a needed request may have failed. Wait for expected content as well as spinner disappearance. Inspect failed requests and the visible error state in DevTools.
The content appears manually but not in automation. The automation may be on another route or frame, or the app may respond differently in that browser context. Check the final URL, frame, viewport, cookies, and request timeline. Make sure the readiness selector belongs to the page being captured.
A fixed delay sometimes works but is unreliable. Page load time varies; the delay can be too short on a slow run and waste time on a fast one. Replace the delay with a page-specific condition where possible. Retain a bounded timeout so a broken page does not wait indefinitely.

Without the target URL, capture code, and a trace, it is not possible to identify which request or app state is responsible for a specific spinner screenshot. Share those details when asking for a site-specific diagnosis.

8. Performance, reliability, and cost

  • Wait for the smallest useful signal. A distinctive content selector can let a screenshot proceed as soon as the needed state is ready; waiting for every resource can add time without improving the image.
  • Bound every wait. Set a navigation timeout and a separate readiness timeout. Treat an unmet condition as a failed capture to investigate, not as permission to save a known loading frame.
  • Make success observable. Record the target URL, readiness selector or condition, timeout, and whether it succeeded. This helps distinguish slow content from a failed request or a selector mismatch.
  • Account for background traffic. Network-idle waits may be slow or never resolve on pages with ongoing activity. Page-specific assertions are often more reliable for those apps.
  • Choose based on volume and setup cost. DIY browser automation requires managing a browser process and matching selectors to the site. ScreenshotNeo offers a per-request API and MCP server; its free tier is 1,000 shots monthly, and paid plans begin at $5 for 3,000. Failed loads and other listed non-clean outcomes are not billed according to the product terms above.

9. Frequently asked questions

Does waitUntil: 'load' guarantee that an app is ready?

No. It is a document navigation milestone. A client-rendered app can still be fetching data or rendering useful content afterward.

Should I always wait for the spinner to disappear?

Only if that spinner reliably represents the loading state. Pair it with a check that the desired content appeared, especially when the app has error states.

Why can network idle be a poor readiness check?

Background requests may prevent idleness even when content is ready, while an idle network does not prove that useful content rendered. Use it only when the target page’s behavior makes it meaningful.

Can you diagnose my exact website from this guide?

A specific diagnosis requires the URL, capture code, and ideally a DevTools network recording or automation trace. The general steps identify what to inspect.