ScreenshotNeo

BlogHow-to

Puppeteer Screenshot of a Single-Page App After Client-Side Rendering

Wait for the SPA state you need, then capture it with Puppeteer. Learn when to use network idle, selectors, or page conditions—and how to fix blank shots.

By the ScreenshotNeo team4 October 20269 min read

To take a Puppeteer screenshot after a single-page app (SPA) has rendered, navigate to the page, wait for a condition that proves the target view is ready, and then call page.screenshot(). For example, wait for the results container and for its loading indicator to disappear. networkidle0 and networkidle2 can help with navigation timing, but neither confirms that a particular client-rendered view is ready.

This guide shows how to capture a full page or an element, choose a readiness condition, handle common SPA timing issues, and automate the capture with Puppeteer. For Puppeteer’s documented screenshot options, see the Screenshots guide and the Page.screenshot API reference.

Capture an SPA after its target view is ready

Use a signal tied to the view you want. A visible results container is a useful start; for a data-driven page, also wait until the loading state ends or the results contain the expected content.

import puppeteer from 'puppeteer';

const url = 'https://example.com/app';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });

  await page.goto(url, { waitUntil: 'domcontentloaded' });
  await page.waitForSelector('[data-testid="results"]', { visible: true });
  await page.waitForFunction(() => {
    const results = document.querySelector('[data-testid="results"]');
    const loading = document.querySelector('[data-testid="loading"]');
    return results && results.children.length > 0 && !loading;
  });

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

Replace the URL and selectors with ones from your app. The example assumes that results are child elements of the results container and that the loading indicator is removed when loading finishes. If your app uses different markup or state, change the predicate to match it. The example composes documented Puppeteer APIs; it does not guarantee that a placeholder selector exists on a real site.

  1. Set a viewport if the screenshot’s width and height matter.
  2. Navigate using an appropriate lifecycle event, such as domcontentloaded.
  3. Wait for a selector or page condition that corresponds to the intended rendered state.
  4. Capture the viewport, full page, or a specific element.
  5. Close the browser in a finally block so it also closes when navigation or capture fails.

Choose the right readiness condition

A page can finish its initial navigation before an SPA finishes fetching data and rendering the view. Conversely, a page may keep making requests after the view is ready. Choose a wait based on what it observes and whether that observation corresponds to the content you need.

Wait method What it observes Good fit Limitation
waitUntil: 'domcontentloaded' The document’s DOM content has been loaded. Start waiting for a client-rendered view promptly, then wait for an app-specific condition. Does not mean the SPA’s data or target component is ready.
waitUntil: 'load' The page’s load event. Pages where load-event resources are part of the needed initial view. Client-side work can continue after the event.
waitUntil: 'networkidle0' No more than zero active network connections for at least 500 ms. Pages that become network-quiet after the relevant requests finish. Polling or long-lived requests can prevent it; network quiet alone does not prove the target view rendered.
waitUntil: 'networkidle2' No more than two active network connections for at least 500 ms. Navigation where a small amount of continuing network activity is expected. Still measures network activity, not a specific application state.
waitForSelector() A matching DOM element, optionally visible. Wait for a known component or loading indicator. The element can exist before it contains final data.
waitForFunction() A page-context function returning a truthy value. Wait for a combination of conditions, such as non-empty results and no loading indicator. The predicate must describe a real, stable success condition in the app.

Puppeteer documents the networkidle0 and networkidle2 thresholds as network-connection conditions sustained for at least 500 ms. They are useful navigation lifecycle choices, but they are not guarantees about a React, Vue, Angular, or other SPA component’s state. Use a page-specific predicate when the screenshot depends on that state. See Puppeteer’s waitForFunction API and Page interactions guide.

Wait for data, not just the container

Apps often render an empty container first, then fill it after a client-side request. Waiting only for the container can capture a blank results area. Add a condition that establishes completion. Depending on your app, that might mean:

  • A loading indicator is gone.
  • A results list has at least one item.
  • A known status element says the view is complete.
  • A particular heading or record appears.

For example, if the app updates the loading indicator’s text rather than removing it, adjust the predicate accordingly:

await page.waitForFunction(() => {
  const status = document.querySelector('[data-testid="status"]');
  const results = document.querySelector('[data-testid="results"]');
  return status?.textContent?.trim() === 'Ready' &&
    results?.children.length > 0;
});

Capture a viewport, full page, or one element

Use a page screenshot for a viewport or the whole document. Use an element screenshot when the output should contain just one component.

Viewport screenshot

await page.screenshot({ path: 'viewport.png' });

The default page screenshot covers the current viewport. Set the viewport before navigation if responsive layout or viewport dimensions affect what the application renders.

Full-page screenshot

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

fullPage defaults to false. A full-page capture is useful for long feeds, reports, and dashboards, but the app may load below-the-fold content lazily. If content appears only after scrolling, you may need to scroll the page before capturing and wait for that content to appear. Keep the desired viewport and app state consistent when producing repeatable comparison images.

Element screenshot

const results = await page.waitForSelector('[data-testid="results"]', {
  visible: true,
});

if (!results) {
  throw new Error('Results element was not found');
}

await results.screenshot({ path: 'results.png' });

An element screenshot scrolls the element into view if needed. Puppeteer documents that capture can fail if the element becomes detached from the DOM, so avoid replacing the component between the wait and screenshot. See the ElementHandle.screenshot API.

Relevant screenshot options

Option Use
path Write the captured image to a file, such as capture.png.
type Select a supported image format, such as PNG or JPEG. Check the current API reference for supported formats.
fullPage Capture the full document instead of only the viewport.
clip Capture a specified rectangular region of the page.
omitBackground Omit the default background where a transparent capture is needed and supported by the output format.
quality Set image quality for formats where the option applies; it does not apply to PNG.

See the current ScreenshotOptions reference for option details and format support.

Why a Puppeteer SPA screenshot is blank or missing content

Symptom Likely cause What to change
Initial shell appears, but data is missing. The capture waits for navigation but not for client-side data or rendering. Wait for a results condition, a completion status, or the end of the loading state.
The wait times out on a page using networkidle0. Polling, analytics, or another continuing request keeps the page active. Use a different navigation event and wait for the application state you need.
The screenshot succeeds, but shows a spinner. The readiness condition only checks that the component exists. Wait for the spinner to disappear or for the component’s contents to meet a completion condition.
One page works but another capture is inconsistent. The pages can resolve requests or render client-side updates at different times. Use a condition tied to each target view instead of relying on an arbitrary short delay.
A full-page capture omits lower-page content. Content may be lazy-loaded only when it is brought into view. Scroll through the page as needed, wait for newly revealed content, then capture.
An element screenshot throws after it was found. The app replaced or removed the element before capture. Wait for the final element state and capture it before a subsequent update detaches it.
The capture has unexpected responsive layout. The viewport was left at a default or changed after rendering. Set the intended viewport before navigation and keep it fixed for comparable captures.

Reliability, performance, and cost considerations

Reliability: A page-specific readiness condition makes the capture’s success criteria explicit. Keep the predicate narrow enough to match the desired state, but strong enough to avoid accepting an empty shell. Always close the browser even if a wait or screenshot fails. For batch jobs, record which URL and condition failed so a timeout can be diagnosed.

Performance: Every extra wait can add time. Use a condition that completes as soon as the required view is ready rather than a long fixed delay. Network-idle waits may be unsuitable for pages with persistent activity. Full-page capture can involve more content than a viewport capture, and element capture can reduce output to the component you need.

Cost: A local Puppeteer script uses the browser and compute resources of the machine or service running it; the exact cost depends on that environment and workload. This research does not establish a universal time or cost benchmark. If you use a hosted screenshot API instead, compare its billing rules and limits against your capture volume and required options.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts one GET request with a URL and can return PNG, JPEG, WebP, or PDF. The Puppeteer target view still needs to be represented by a URL and supported capture options; for app-specific readiness conditions or authenticated states, check the ScreenshotNeo API documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/app"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/app',
});
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);

These examples use the same API endpoint and request parameters in cURL, Python, and Node.js. In Node.js environments without Bun.write, save the response body using your preferred file-writing method. Add an appropriate access key before making the request.

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. See ScreenshotNeo for product details and the docs for API options.

Sign up for 1,000 free screenshots a month, with no card.

FAQ

How do I take a Puppeteer screenshot after a React, Vue, or Angular app has rendered?

Wait for a state that indicates the view you need is complete, such as non-empty results or a completed loading status, then call page.screenshot(). The framework name does not change the core approach.

Should I use networkidle0 or networkidle2?

Use one only when its network-activity condition fits the page. networkidle0 permits no active connections and networkidle2 permits up to two, each for at least 500 ms. For a specific SPA view, an app-level selector or predicate is usually a more direct readiness check.

How do I wait for an element before taking a full-page screenshot?

Call page.waitForSelector(selector, { visible: true }), then page.screenshot({ path: 'capture.png', fullPage: true }). If the element can appear before its data is ready, also wait for a completion condition.

Can Puppeteer capture only one component?

Yes. Wait for the element, then call its screenshot() method. The element is scrolled into view if needed, and capture can fail if the app detaches it before the screenshot is taken.