ScreenshotNeo

BlogHow-to

How to Wait for JavaScript to Finish Before an HTMLCSStoImage Capture

Make HTMLCSStoImage capture dynamic content only after it is ready. Compare explicit readiness signals, URL markers, fixed delays, and browser automation waits.

By the ScreenshotNeo team4 October 20267 min read

To wait for JavaScript before an HTMLCSStoImage capture, use an explicit ready signal when you can. For submitted HTML and CSS, set render_when_ready: true and call ScreenshotReady() after the JavaScript work that must appear in the image has finished. For a URL capture, add an element with the ID HCTIReadyNow when the page is ready; the helper itself is unavailable for URL captures. If you cannot change the page or add a signal, use ms_delay as a fixed-pause fallback, starting at 500 ms and increasing if needed. A delay is only an estimate.

This guide covers HTMLCSStoImage’s readiness options and, where you run the browser yourself, how to wait for a meaningful page state with Playwright or Puppeteer. See the HTML/CSS to Image documentation for the current API details.

1. Choose a wait method

Method Use it when What it guarantees
render_when_ready plus ScreenshotReady() You submit HTML/CSS and control its JavaScript Your code signals readiness. It only covers work completed before you call the helper.
HCTIReadyNow marker You control the page captured by URL The page inserts the marker at the state you choose.
ms_delay You cannot signal readiness and the extra time needed is reasonably predictable A pause for the configured interval, not proof that JavaScript finished.
Browser automation wait You manage a Playwright or Puppeteer browser Waits for the selector or application marker you specify.

The service’s normal readiness heuristic waits for the page load event and then monitors additional network traffic, including external CSS and images. This often works, but later API responses, charts, and client-side updates can still be pending. There is no single browser event that means every application’s JavaScript is finished.

2. Submitted HTML and CSS: signal readiness explicitly

Set render_when_ready to true in the API request. In the HTML’s JavaScript, call ScreenshotReady() only after all content intended for the image has been added and rendered. This runnable example waits for its asynchronous data, inserts it, then signals readiness.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Ready capture example</title>
</head>
<body>
  <main>
    <h1>Monthly signups</h1>
    <div id="result">Loading…</div>
  </main>
  <script>
    async function render() {
      try {
        // Replace this with the API call or rendering work your page needs.
        const values = await Promise.resolve([12, 18, 25]);
        document.querySelector('#result').textContent = values.join(', ');
      } catch (error) {
        document.querySelector('#result').textContent = 'Unable to load data';
      } finally {
        // Signal only when the intended final state is in the document.
        ScreenshotReady();
      }
    }
    render();
  </script>
</body>
</html>

For a chart, put the call in the chart library’s completed-render callback. For data-driven HTML, call it after the response has been handled and the DOM updated. If several independent tasks affect the image, wait for all of them before signaling; calling from only one task can capture the others midway through.

When the capture request itself uses a JSON body, include the submitted HTML/CSS fields required by your account’s API setup and set render_when_ready to true. The ready helper is part of the submitted page workflow; it is not a generic browser function for arbitrary remote URLs.

3. URL capture: add the readiness marker to the page

For URL-to-image, configure render_when_ready: true and have the page add an element whose ID is HCTIReadyNow only after the content is ready. For example, add the marker after the API response has been rendered:

<script>
async function updatePage() {
  const response = await fetch('/api/report');
  const report = await response.json();
  renderReport(report);

  const marker = document.createElement('div');
  marker.id = 'HCTIReadyNow';
  document.body.appendChild(marker);
}
updatePage();
</script>

The example assumes renderReport completes the visible update synchronously. If it starts more asynchronous rendering, put marker insertion in that rendering’s completion path instead. The marker’s presence only represents the tasks you chose to finish before inserting it.

4. Fixed delay fallback

When you cannot modify the page to expose readiness, set ms_delay. The service FAQ recommends starting with 500 milliseconds and adjusting upward as required. Use the smallest delay that reliably covers the known work; a fixed pause can waste time on fast runs and still be too short on slow ones.

{
  "url": "https://example.com/report",
  "render_when_ready": true,
  "ms_delay": 500
}

max_wait_ms is documented as a maximum time limit in the range 500–10000 milliseconds. Treat it as an upper bound on waiting, not a request to wait for that entire duration. Check the current parameter documentation for how these settings interact in your request mode.

5. If you manage the browser, wait for the content that matters

In Playwright or Puppeteer, do not treat navigation completion alone as proof that application work is done. Wait for a selector whose state means the useful content has arrived, such as a populated row or an application-owned ready marker. Waiting for an empty container is insufficient if data is inserted later.

Playwright example

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
  await page.goto('https://example.com/report', { waitUntil: 'load' });
  // Use a selector that only appears once the report data is rendered.
  await page.locator('[data-report-ready="true"]').waitFor({ state: 'attached', timeout: 15000 });
  await page.screenshot({ path: 'report.png', fullPage: true });
} finally {
  await browser.close();
}

Puppeteer example

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/report', { waitUntil: 'load' });
  // Replace this with a selector that represents completed application content.
  await page.waitForSelector('[data-report-ready="true"]', { timeout: 15000 });
  await page.screenshot({ path: 'report.png', fullPage: true });
} finally {
  await browser.close();
}

These examples assume the page adds data-report-ready="true" at the point your application considers the report complete. The selector and timeout are application choices; neither a generic selector nor a generic timeout can establish readiness for every site.

6. Common errors and fixes

Symptom Likely cause Fix
Image contains a loading state or missing data The ready helper or marker was added before asynchronous work finished. Move the signal into the final API, chart, or DOM-update completion path. Include every task that affects the screenshot.
ScreenshotReady is undefined The page is a URL capture, or the helper is called outside the submitted HTML/CSS rendering workflow. For URL capture, use HCTIReadyNow. For submitted HTML/CSS, enable render_when_ready and use the provided helper in that flow.
URL capture does not finish waiting The page never adds the expected marker, or it adds it to a context the capture does not observe. Insert the correctly named element into the rendered document only when ready, and verify the code path runs on the captured page.
A 500 ms delay works inconsistently Network or render duration varies; 500 ms is a starting point, not a guarantee. Prefer an explicit signal. If unavailable, increase ms_delay based on observed behavior and account for slower runs.
Automation times out waiting for a selector The selector is wrong, never appears, or describes a state the page does not reach. Inspect the rendered DOM and choose a marker tied to successful content completion. Handle failed API responses so the page can expose an error state too.
Screenshot is taken after a container appears but before it fills The wait observes structural presence rather than useful content. Wait for a populated child, a meaningful attribute/state, or an application-owned completion marker.

7. Reliability, speed, and cost considerations

  • Prefer signals over guessed time. A readiness signal follows the actual completion path; a fixed delay is vulnerable to variable network and rendering time.
  • Make the signal cover the screenshot. A marker cannot account for work the application has not awaited. Decide whether images, fonts, chart painting, or other late updates must be visible and include their completion in the ready condition.
  • Set a sensible timeout. A missing signal can otherwise make capture behavior difficult to diagnose. The documented max_wait_ms range is 500–10000 ms; choose within supported limits and handle cases where the page never reaches readiness.
  • Keep work bounded. Do not wait for unrelated polling or background requests if they do not change the captured result. A meaningful selector or marker can avoid waiting for activity that never becomes globally idle.
  • Handle failure as a state. If the data request fails, render a useful error state and signal that state as ready when it is what the screenshot should show. Otherwise a failed request may leave the capture waiting or displaying a spinner.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-call API can return an image or PDF; the parameter names used by other screenshot APIs also work, which can make switching easier. See the ScreenshotNeo 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,
)
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 banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use screenshot tools, and 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, with no card.

9. FAQ

Does the page load event mean JavaScript has finished?

No. Later API calls, client-side updates, and rendering work can continue after load.

Can I use ScreenshotReady() for a URL capture?

No. For URL capture, add the HCTIReadyNow element when the page reaches the desired state.

Is 500 ms enough?

It is the documented starting point for ms_delay, not a universal wait duration. Use an explicit readiness signal where possible.

Should I wait for network idle?

Only if network quiet corresponds to the content you need. Pages with polling or unrelated requests can remain active, while an application may render the needed result before all network activity stops.