ScreenshotNeo

BlogHow-to

How to Capture a Web App Screenshot After React Finishes Rendering in Puppeteer

Wait for the React state your screenshot needs, then capture it with Puppeteer. Includes readiness signals, runnable code, troubleshooting, and alternatives.

By the ScreenshotNeo team4 October 20267 min read

To capture a React app reliably with Puppeteer, wait for a condition that represents the exact app state you want to show, then take the screenshot. The strongest signal is an application-owned readiness flag or marker that is set only after required data has loaded and the relevant UI has rendered. A selector, network-idle wait, or short delay can help in narrower cases, but none automatically means React is done for your use case.

1. Define what “ready” means

React does not expose a universal Puppeteer-ready flag. A page can have loaded its HTML while React is still fetching data, rendering a route, or replacing a loading state. Decide what the screenshot must contain, then make that state observable in the page.

For a dashboard screenshot, “ready” might mean the dashboard data request succeeded, the chart has rendered, and the loading indicator is gone. Set a marker only after those requirements are true. The marker below is an example contract that your application must implement; it is not built into React or Puppeteer.

2. Runnable Puppeteer example

Install Puppeteer in a Node.js project, then save this as an ES module such as screenshot.mjs. Set SCREENSHOT_URL to your app route. The app must set window.__APP_READY__ to true when the target view is actually ready.

import puppeteer from 'puppeteer';

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

try {
  const page = await browser.newPage();
  page.setDefaultTimeout(30_000);

  await page.goto(url, { waitUntil: 'domcontentloaded' });

  await page.waitForFunction(
    () => window.__APP_READY__ === true,
    { timeout: 30_000 },
  );

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

waitForFunction() repeatedly evaluates a predicate in the page context until it returns a truthy value. Its wait timeout is configurable; set a finite timeout so a broken readiness contract fails clearly instead of hanging. See Puppeteer’s waitForFunction API and wait options.

Expose the marker from the app

Implement the marker where your app already knows that the required data and UI are ready. For example, a route-level component can set it after its data has loaded and its target content is represented in the DOM. Clear the flag when beginning a new load or changing to a different view so a previous ready state cannot satisfy the next capture.

// Example client-side contract. Place this in the app's readiness logic.
// Set ready only after required data has loaded and the view is rendered.
window.__APP_READY__ = false;

async function loadDashboard() {
  const data = await fetchDashboardData();
  renderDashboard(data);
  window.__APP_READY__ = true;
}

The renderDashboard call here stands for your app’s actual state update. If the update schedules a later React render, set the marker from a post-render effect or use a DOM marker rendered only for the desired state. Do not set it merely because a request returned if the visible UI is not yet updated.

3. Choose the right readiness signal

Signal Use it when Limit
Application flag or marker You control the app and can encode required data and UI state. Requires an explicit app-side contract and correct reset behavior.
Selector or locator A stable element appears only when useful content is ready. Presence alone may happen before data is complete or before a later update.
Network idle Relevant page work normally ends when network activity quiets. Polling or analytics can prevent idleness; quiet traffic does not prove the target UI is complete.
Fixed delay You are diagnosing timing in a quick one-off capture. It may be too short on a slow run and wastes time on a fast one.

Wait for a meaningful selector

If your app renders a target element only when it is ready, wait for that element and then capture it or the page. For a page screenshot:

await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="dashboard-ready"]', {
  visible: true,
  timeout: 30_000,
});
await page.screenshot({ path: 'dashboard.png', fullPage: true });

A selector should reflect the needed state rather than a generic shell element that exists during loading. Puppeteer locators can also check element state and stable bounding boxes across animation frames, which helps with interaction readiness; it does not establish that your data requirements have been met. See the Puppeteer page interactions guide.

Use network idle as an additional condition

Puppeteer supports navigation lifecycle conditions such as networkidle0 and networkidle2, as well as page.waitForNetworkIdle(). These describe network activity, not completion of app-specific rendering. If network settling matters, combine it with the app condition:

await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__APP_READY__ === true, {
  timeout: 30_000,
});
await page.waitForNetworkIdle({ idleTime: 500, timeout: 10_000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });

Use a supplementary network wait only if the page’s behavior supports it. A site with background polling may never become idle, while a quiet page can still update after a timer or state transition. Refer to Puppeteer’s waitForNetworkIdle API.

Why arbitrary sleeps are weak readiness checks

await new Promise(resolve => setTimeout(resolve, 3000)) waits three seconds regardless of whether the app is ready. A slower run can still show a loading state; a faster run pays unnecessary delay. Use a fixed pause only for a known animation or as a temporary diagnostic, and keep a real readiness condition as the gate.

4. Capture the whole page, a region, or one component

Use fullPage: true for a full-page image. Puppeteer also supports clipping a rectangle with clip. For a component, wait for its target and take an element screenshot:

const target = await page.waitForSelector('[data-testid="summary-card"]', {
  visible: true,
  timeout: 30_000,
});
if (!target) throw new Error('Summary card was not found');
await target.screenshot({ path: 'summary-card.png' });

An element screenshot scrolls the element into view when necessary. If the element becomes detached before capture, Puppeteer reports an error; wait for the final element instance or locate it again immediately before the screenshot. See the Puppeteer screenshots guide, ElementHandle screenshot API, and ScreenshotOptions.

5. Troubleshooting

Symptom or error Likely cause Fix
Screenshot shows a spinner or loading shell The gate checks navigation or a shell element, not the target app state. Wait for an app-owned flag or a marker rendered only after required data and UI are ready.
waitForFunction times out The app never sets the flag, sets it under a different route, or the page failed before the state was reached. Inspect page errors and network responses, verify the flag in the browser context, and ensure the app sets it on success and handles failure explicitly.
waitForSelector times out The selector is wrong, the element is absent, or it is not visible before timeout. Confirm the route and selector in the rendered DOM; use a marker whose visibility matches the desired capture state.
networkidle0 never resolves Long polling, streaming, or recurring requests keep the network active. Use the application readiness condition as the primary gate; omit network idle or give it a separate bounded timeout.
Capture is blank despite a successful wait The condition became true too early, or the page rendered an empty/error state. Make readiness require the actual content and expected data state; distinguish successful empty data from a failed request.
Element screenshot says the node is detached React replaced the node between lookup and capture. Wait for the final state, query the element again just before capture, and avoid triggering a state change during the operation.
Intermittent missing chart, font, or image The readiness condition covers app data but not a late visual asset. Add an app-specific condition for that asset or wait for the relevant image/font work where needed; do not assume a generic network-idle signal proves every visual is ready.

6. Performance, reliability, and cost

  • Performance: Start navigation with the earliest useful lifecycle point, then wait only for the specific state needed. Waiting for every possible request can add latency and be blocked by unrelated background traffic.
  • Reliability: Keep the wait bounded and fail the capture when the readiness contract is not met. Record the URL and error in your own job logs so timeouts can be diagnosed. Reset readiness markers between navigations and application state changes.
  • Repeatability: Use a stable test account and predictable app data for recurring captures. If animations alter the frame, coordinate capture timing with the app’s own state or disable the relevant animation for the capture environment.
  • Cost: Puppeteer is an open-source browser automation library, but running a browser still uses compute, memory, storage, and maintenance time. The required budget depends on your hosting and capture volume; no universal runtime or price applies.

7. Or skip the browser setup

For a one-call capture, ScreenshotNeo accepts a URL and returns an image or PDF. Its API supports PNG, JPEG, and WebP output, full-page capture, custom waits, and many other capture options. See the ScreenshotNeo website and API documentation.

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,
)
r.raise_for_status()
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}`);
await Bun.write('shot.webp', res);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed. 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, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

FAQ

Does React provide a built-in screenshot-ready event?

No universal event is established by the Puppeteer APIs. Define an app-side condition that represents the view you need to capture.

Should I use networkidle0 or networkidle2?

Use one only when network quiet is relevant to the page. Neither setting verifies that a particular React state has rendered; an app marker is a better primary gate.

What should happen when readiness times out?

Treat the capture as failed and inspect the route, app marker, data requests, and page errors. Increasing the timeout helps only when the intended state is eventually reached and simply needs more time.