ScreenshotNeo

BlogHow-to

Capture a website screenshot after JavaScript content finishes rendering

Wait for the page-specific signal that your JavaScript content is ready, then capture it with Playwright or Puppeteer. Includes runnable examples and fixes for common timing issues.

By the ScreenshotNeo team4 October 20268 min read

To capture JavaScript-rendered content reliably, navigate to the page, wait for a condition that proves the content you need is present, and then take the screenshot. A navigation milestone such as load, or a quiet network, does not necessarily mean a client-rendered application has finished the work relevant to your capture.

The examples below use Playwright and Puppeteer. They wait for a page-specific selector before taking the screenshot. Use a selector that represents the actual content you need, such as a results container or a report heading.

1. Install a browser automation library

Choose the library that fits your existing project. The examples use Node.js and save a PNG file.

# Playwright
npm install playwright
npx playwright install chromium

# Or Puppeteer
npm install puppeteer

Use one of these installations for the corresponding example; you do not need both libraries.

2. Capture after the content is ready with Playwright

Save this as screenshot.mjs. Replace the URL and selector with the page and content container you need. The example waits for the selector to become visible, then saves the screenshot.

import { chromium } from 'playwright';

const url = 'https://example.com/dashboard';
const readySelector = '[data-testid="dashboard-content"]';

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

  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
  await page.locator(readySelector).waitFor({
    state: 'visible',
    timeout: 20_000,
  });

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

Run it with node screenshot.mjs. The try/finally ensures the browser closes even if navigation or the readiness wait fails.

For a particular element, wait for that element and capture its bounding box:

const card = page.locator('[data-testid="summary-card"]');
await card.waitFor({ state: 'visible', timeout: 20_000 });
await card.screenshot({ path: 'summary-card.png' });

3. Capture after the content is ready with Puppeteer

Save this as screenshot.mjs if using Puppeteer. Its waitForSelector resolves when a matching element appears; set visible: true when the element must also be visible.

import puppeteer from 'puppeteer';

const url = 'https://example.com/dashboard';
const readySelector = '[data-testid="dashboard-content"]';

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

  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
  await page.waitForSelector(readySelector, {
    visible: true,
    timeout: 20_000,
  });

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

Run it with node screenshot.mjs. To capture just an element, Puppeteer supports an element handle screenshot:

const element = await page.waitForSelector('[data-testid="summary-card"]', {
  visible: true,
  timeout: 20_000,
});
await element.screenshot({ path: 'summary-card.png' });

See the official Playwright Page documentation and Puppeteer screenshot guide for the APIs and options.

4. Choose a readiness signal that matches the page

A screenshot records the browser’s rendered state at capture time. Pick a wait that corresponds to the content in the image:

Wait condition What it tells you When it fits
domcontentloaded The initial document has been parsed. Use as an early navigation milestone when you will follow it with a page-specific wait.
load The document load event has fired. Useful as a general milestone, but app work can continue afterward.
networkidle Playwright defines this as no network connections for at least 500 ms. Use only if network quiet is meaningful for this page. Playwright discourages it as a general test-readiness strategy.
Visible selector A matching element exists and is visible. Usually a clear choice when the screenshot depends on a known content region.
Application-ready marker Your app reports that the required work is complete. Best when you control the app and can expose a reliable marker.
Fixed delay A set duration has elapsed. Fallback when there is no observable signal; it can waste time or still be too short.

Playwright’s documentation recommends using web assertions to assess readiness rather than treating networkidle as a general test-ready signal. A selector or application-specific condition is more directly tied to the intended output. See navigation wait conditions in the Playwright Page docs.

Wait for text or a loading state

If the app renders a stable heading when data is ready, wait for that text or a locator that contains it. If it displays a loading indicator, you can wait for the indicator to disappear, then verify the result container is visible. A marker you control, such as data-ready="true", can make the contract explicit.

// Example: wait for an app-owned readiness marker.
await page.locator('main[data-ready="true"]').waitFor({
  state: 'visible',
  timeout: 20_000,
});

This works only if the application sets the marker after the content needed for the capture is ready. The selector examples above use a visible locator; for other conditions, use the library’s locator or assertion APIs and keep the condition specific to the page.

When a short delay is unavoidable

When the page exposes no useful signal, a delay can provide a simple fallback. It does not prove that rendering finished, so choose a conservative value and expect variation under slow network or overloaded pages. Prefer an observable condition whenever possible.

await page.waitForTimeout(1_000); // Playwright fallback only

5. Set capture options for repeatable output

  • Viewport: Set width and height before navigation or capture. Responsive layouts can change at different sizes.
  • Device scale: Use a consistent device scale factor when pixel dimensions matter.
  • Full page or viewport: Set fullPage: true when the entire document is needed; omit it for the current viewport.
  • Element capture: Capture a specific container when surrounding page content is irrelevant.
  • Browser environment: Keep browser version, operating system, settings, hardware, and headless mode consistent for visual comparisons. Playwright notes that these can affect rendering.
  • Dynamic content: If data changes on each run, use a stable fixture or test account when comparing screenshots.

For Playwright Test visual regression checks, toHaveScreenshot() waits for two consecutive screenshots to match before comparing to the expectation. This helps with transient rendering changes, but it does not replace waiting for the page-specific content that matters. Read Playwright’s visual comparisons guide.

6. Use cURL, Python, or Node.js with a screenshot API

cURL, Python, and Node.js can call an HTTP screenshot API, but they do not themselves wait for page JavaScript. The screenshot service must perform browser rendering and readiness handling. For a do-it-yourself browser session with a custom readiness selector, use the Playwright or Puppeteer examples above.

cURL

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

Python

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)

Node.js

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());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

These examples save the default image response. To configure output and other capture behavior, use the ScreenshotNeo API documentation. Its API supports 63 options, including full-page capture with lazy images loaded, CSS selector element capture, viewport and device presets, custom CSS or JavaScript, selector or delay waits, request blocking, headers, cookies, user agent, caching, and more. Use the documented parameter names and values for the exact option you need.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Make one request for a screenshot:

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

See the API docs for parameters and response details. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

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

Troubleshooting

Symptom Likely cause Fix
The screenshot shows a spinner or skeleton. The wait condition fires before data-backed rendering completes. Wait for the final content container, loaded text, or app-ready marker rather than only for navigation.
The selector wait times out. The selector is wrong, appears only after another interaction, or is inside a frame. Inspect the page DOM, verify the selector, handle required interactions, and target the correct frame if applicable.
The selector exists but the result is still incomplete. The container appears before its children or data finish rendering. Wait for a child element or text that specifically indicates the required data is present.
networkidle never arrives. The site may keep requests open or continue background traffic. Use a page-specific selector or app signal instead of waiting for general network quiet.
The screenshot is blank or navigation fails. The page may have failed to load, redirected, or rejected the browser session. Check the final URL and page errors, confirm the target is reachable from the capture environment, and raise the navigation timeout only when slow response is expected.
Images are missing in a full-page capture. Lazy-loaded images may not load until their region is approached. Scroll the page or use a capture setup that loads lazy images before capturing, then verify the final page state.
Screenshots differ across runs. Fonts, animations, timestamps, data, browser versions, or the host environment vary. Use stable data, disable or settle animations where appropriate, and keep the rendering environment consistent.
Element capture fails or clips the wrong area. The target is hidden, detached, or changes size around capture time. Wait until visible, ensure it remains attached, and capture after its dimensions stabilize.

Performance, reliability, and cost

Waiting for a relevant selector is generally more efficient than adding a large fixed delay: fast pages can proceed as soon as the condition is met, while slow pages remain bounded by a timeout. A selector timeout should fail visibly instead of silently producing an incomplete image. Set navigation and readiness timeouts according to the page’s expected behavior, and close the browser in a finally block so failed runs do not leave browser processes behind.

For repeated captures, reuse a browser process where your job architecture allows it, but isolate page state when cookies, authentication, or user data differ. Keep viewport and browser versions stable for visual checks. A hosted screenshot API removes the need to manage local browser installation and execution; compare its pricing and capture behavior against the runtime and maintenance cost of your own browser setup. ScreenshotNeo’s listed plans are Free (1,000 shots/month), Starter ($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. Its response includes page-verdict and billing headers, and only clean shots are billed; cache hits and failed outcomes such as bot checks, blank pages, timeouts, and failed loads cost nothing.

FAQ

Does load mean a single-page app is ready?

No. Client-side rendering and later data requests can continue after the document load event. Wait for the content the screenshot needs.

Should I use Playwright or Puppeteer?

Either can navigate and capture screenshots. Choose based on your existing framework and language, browser needs, and whether you need Playwright Test’s visual assertion workflow.

Can I capture one element instead of the full page?

Yes. Both examples show element capture. Wait for the target to be visible before capturing it.

Why is a screenshot different on another machine?

Rendering can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Keep the environment consistent for comparisons.