ScreenshotNeo

BlogHow-to

How to Wait for a Single-Page App to Render Before a Scheduled Screenshot

Wait for the content your screenshot needs, not just a browser load event. This guide shows reliable Playwright and Puppeteer patterns for scheduled SPA captures.

By the ScreenshotNeo team4 October 202610 min read

Before a scheduled screenshot, wait for a condition that proves the content you want to capture has rendered. For a single-page app (SPA), a browser navigation event such as load only describes document loading; it does not guarantee that client-side data, hydration, or the target view is ready. Prefer a page-specific signal, such as a visible content locator or a JavaScript predicate that checks the intended state.

This guide uses Playwright with Node.js for a complete scheduled-capture example, then shows equivalent Puppeteer and Python patterns. It also covers timeout handling, schedules, reliability, and what to check when a capture contains a loading state.

1. Choose a readiness signal that matches the screenshot

Start by identifying what must be true for the screenshot to be useful. Examples include a report heading being visible, a table containing rows, or an application-specific loading indicator disappearing. A useful signal describes the content or state you need, rather than merely the existence of the page shell.

Signal What it indicates Is it enough for an SPA screenshot?
commit The response arrived and document loading started. Usually too early to establish that visible app content is ready.
domcontentloaded The initial document was parsed. Useful as a navigation milestone, but it does not prove that client-side data has rendered.
load The browser’s load event fired. Can be useful, but it is not a universal SPA readiness signal.
networkidle A network quiet period was reached. Not a universal readiness signal. Playwright describes a 500 ms quiet period and discourages using it to assess readiness.
App-specific locator or predicate The page reached a condition you define. Usually the best choice when it accurately represents the state to capture.

Playwright recommends web assertions for readiness rather than relying on networkidle. Some apps keep analytics, polling, or streaming connections open; others can become briefly quiet before the desired data appears. Puppeteer’s screenshot guide demonstrates navigation using networkidle2, but that example is not proof that every SPA is ready. Use a page-specific condition as the decisive gate when possible. Playwright Page API · Puppeteer Screenshots guide

2. Scheduled screenshot with Playwright and Node.js

The following script navigates to a page, waits for a meaningful locator, and writes a screenshot. Replace the URL and selector with values from your app. The selector should identify the finished content, not a generic app container that appears before data loads.

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

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

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

    // Choose a locator that appears only when the report content is ready.
    await page.locator('[data-testid="report-content"]').waitFor({
      state: 'visible',
      timeout: 30_000,
    });

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

capture().catch((error) => {
  console.error('Scheduled screenshot failed:', error);
  process.exitCode = 1;
});

Install Playwright and its browser in the environment that runs the job, following the Playwright installation guide. This example uses CommonJS. If your project uses ECMAScript modules, import chromium with import { chromium } from 'playwright'; and keep the rest of the flow the same.

Wait for text or a loading indicator

If the app has a stable heading that appears with the final view, use its role and accessible name. If a loading indicator reliably means the content is still being fetched, wait for it to disappear and also verify the target content exists.

await page.getByRole('heading', { name: 'Monthly report' }).waitFor({
  state: 'visible',
  timeout: 30_000,
});

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

await page.locator('[data-testid="report-table"] tbody tr').first().waitFor({
  state: 'visible',
  timeout: 30_000,
});

Pick the condition that actually corresponds to the screenshot’s purpose. A heading may appear before its data; a hidden loader may disappear because of an error. When possible, check both that loading has ended and that the expected content is present.

Wait for a page-specific JavaScript condition

When readiness cannot be represented by one locator, use a predicate exposed by the app or evaluate a condition based on rendered DOM state. For example, if the page adds a data-ready attribute after it has populated the view:

await page.waitForFunction(() => {
  const report = document.querySelector('[data-testid="report-content"]');
  return report?.getAttribute('data-ready') === 'true';
}, { timeout: 30_000 });

The predicate must be safe to evaluate in the page and should return true only when the screenshot’s required state is ready. Prefer a public app signal over assumptions about internal framework variables.

Capture a single element

If the scheduled artifact should contain one chart, card, or report panel, capture that locator rather than the whole page:

await page.locator('[data-testid="revenue-chart"]').screenshot({
  path: 'revenue-chart.png',
});

Playwright actions and locators auto-wait for relevant action conditions, but that behavior does not establish that an SPA has rendered the specific state your screenshot needs. Keep the explicit readiness wait when capture timing depends on it. See the Playwright Page API.

3. Puppeteer version

Puppeteer also supports waiting on a locator or a page condition before capture. This runnable CommonJS example waits for the target content and then takes a full-page screenshot:

const puppeteer = require('puppeteer');

async function capture() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  page.setDefaultTimeout(30_000);

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

    await page.locator('[data-testid="report-content"]').wait();

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

capture().catch((error) => {
  console.error('Scheduled screenshot failed:', error);
  process.exitCode = 1;
});

For an app-specific condition, Puppeteer offers waitForFunction:

await page.waitForFunction(() => {
  const report = document.querySelector('[data-testid="report-content"]');
  return report?.getAttribute('data-ready') === 'true';
}, { timeout: 30_000 });

To capture just an element, wait for it and use its screenshot method:

const chart = await page.waitForSelector('[data-testid="revenue-chart"]', {
  visible: true,
  timeout: 30_000,
});
await chart.screenshot({ path: 'revenue-chart.png' });

See Puppeteer’s official page interactions guide, Page API, and screenshot guide for the APIs and examples.

4. Python Playwright version

If your scheduled job is written in Python, use the synchronous Playwright API to wait for the same kind of page-specific content. Install Playwright and its browser in the job environment using the Python installation guide.

from playwright.sync_api import sync_playwright


def capture():
    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=True)
        page = browser.new_page(viewport={"width": 1440, "height": 1000})
        try:
            page.goto(
                "https://example.com/reports",
                wait_until="domcontentloaded",
                timeout=30_000,
            )
            page.locator('[data-testid="report-content"]').wait_for(
                state="visible",
                timeout=30_000,
            )
            page.screenshot(path="scheduled-report.png", full_page=True)
        finally:
            browser.close()


if __name__ == "__main__":
    capture()

For a custom app condition, use page.wait_for_function and return a value that becomes truthy only when the desired view is ready. Keep a finite timeout so a broken or unauthenticated page does not leave the scheduled process hanging.

5. Make the capture a reliable scheduled job

  1. Define the expected state. Choose the selector, text, row count, or app readiness flag that means the screenshot is useful.
  2. Choose a navigation milestone. Use domcontentloaded or another appropriate navigation condition to get to the document, then wait separately for the app state.
  3. Set an intentional timeout. Tune it to your site’s normal behavior and schedule. There is no universal timeout value; the official documentation does not prescribe one for all sites.
  4. Capture only after the condition passes. Take a full-page or element screenshot as required.
  5. Make failure visible to the scheduler. Log the URL and error, return a nonzero process status on failure, and configure the scheduler to retain the job result or notify its operator.
  6. Keep the browser lifecycle bounded. Close the page or browser in a finally block so a failed wait does not leave a browser process behind.
  7. Use a stable runtime environment. Install the matching browser dependencies, provide any required credentials through the job’s secret mechanism, and ensure the output directory is writable.

The examples above are the job’s capture process; invoke that script from your scheduler at the desired cadence. The scheduling mechanism is separate from browser readiness: regardless of whether a job runs from a system scheduler or a hosted workflow, keep the readiness condition and failure status in the script.

6. Performance, reliability, and cost

Performance

A specific locator or predicate usually lets a job proceed as soon as the required state exists. A long fixed sleep adds its full delay to every run and can still be too short on a slow run. Avoid waiting for unrelated page activity when the screenshot only depends on one known component.

Reliability

Use selectors that are stable across visual redesigns, such as a deliberate test ID or accessible role and name. A timeout should fail the capture clearly rather than silently produce an image of a spinner. For content that can legitimately be empty, make the readiness condition distinguish a valid empty state from a failed load.

Pages that require login need a deliberate authentication setup in the job. Confirm that the job is seeing the same route, permissions, locale, and application state expected by the screenshot. A login redirect or access-denied view can otherwise satisfy generic document events while the target content never appears.

Cost

Browser automation consumes the runtime allocated to the scheduled job, and each run must start or access a browser and render the page. The dossier contains no benchmark or universal cost figure, so measure job duration and resource use in your own environment. Reducing unnecessary waits and capturing only the needed content can reduce wasted work.

7. Troubleshooting scheduled SPA screenshots

Symptom Likely cause Fix
The screenshot shows a spinner or skeleton. The script waited for document navigation, not application readiness. Wait for the actual content locator or a page-specific readiness predicate.
The locator wait times out. The selector is wrong, the route differs, the content never loads, or the job is unauthenticated. Check the rendered page and route in the same job environment; verify credentials and choose a selector present in the intended state.
networkidle never happens. The page may keep requests open or continue polling. Use a locator or app-specific predicate instead of network quiet as the only gate.
The screenshot is intermittently incomplete. The readiness signal may fire before all screenshot-critical data or transitions finish. Strengthen the condition to include the needed data, and add only a short bounded stabilization wait if a known animation requires it.
The screenshot succeeds but shows an error or empty state. The chosen condition may match both success and failure, or the app may have failed gracefully. Check for expected content or a success marker and detect the app’s error state separately.
The scheduled process hangs or leaves browser processes. A wait has no practical bound or cleanup does not run on errors. Use finite navigation and readiness timeouts, close the browser in a finally block, and propagate failure to the scheduler.
It works locally but fails in the scheduled environment. The job may lack browser dependencies, secrets, writable output storage, or the same network access. Check those environment differences and log the failing URL and error without exposing credentials.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return an image or PDF, and its wait-for-selector option can wait for page content before capture. The API uses the same readiness principle: choose a selector that represents the rendered state you need. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/reports -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/reports"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/reports' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

These minimal examples show the one-call capture; configure the wait-for-selector option and other capture settings using the docs. ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

9. FAQ

Should I wait for a specific amount of time after the page loads?

Only when you have a known short transition or animation to let settle, and pair that bounded delay with a readiness condition. A delay alone does not prove the target content arrived.

Can Playwright’s auto-waiting replace an explicit readiness check?

Auto-waiting helps actions interact with elements in an appropriate state. For screenshots, explicitly wait for the page condition that represents the intended rendered content.

What if the page has no stable selector?

Ask whether the app can expose a stable readiness attribute or use a meaningful accessible role and name. A page-specific JavaScript predicate is another option when it observes a stable state.

Is a full-page screenshot always the right capture?

No. Use an element screenshot when the scheduled artifact only needs a particular chart or panel; this can also avoid capturing unrelated page content.