ScreenshotNeo

BlogHow-to

How to Fix Screenshots of Websites That Require JavaScript to Render

Fix blank screenshots of JavaScript-rendered websites by enabling JavaScript, waiting for the right page state, and handling frames and delayed content.

By the ScreenshotNeo team4 October 20269 min read

A screenshot of a JavaScript-rendered site can be blank or incomplete if capture happens before the browser has run the page’s scripts and rendered the content you need. Enable JavaScript before navigating, then wait for a page-specific signal such as a target element becoming visible or expected text appearing. A generic load event or network-idle period can help diagnose timing, but neither proves that every application is ready.

This guide uses Puppeteer for the do-it-yourself method. It covers readiness checks, frames, lazy content, repeatable captures, and common failures. Puppeteer’s screenshot guide shows navigation followed by Page.screenshot(); its example uses networkidle2, but that is only one possible strategy. Puppeteer screenshot guide.

1. Enable JavaScript and wait for the content you need

Use a real browser automation engine, make sure scripts are enabled, navigate, and wait for an observable page condition before taking the screenshot. The selector below is illustrative: replace it with a marker that actually appears when the target page is ready.

const puppeteer = require('puppeteer');

(async () => {
  const url = 'https://example.com';
  const browser = await puppeteer.launch({ headless: true });

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

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

    // Replace this with a real readiness marker from the target application.
    await page.waitForSelector('[data-page-ready="true"]', {
      visible: true,
      timeout: 15000,
    });

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

Install Puppeteer in your project using its current installation instructions, then run the script with Node.js. The target page must expose the selector used in the example; there is no universal selector that means “all JavaScript has finished.” If you change the JavaScript setting after a page has already loaded, Puppeteer documents that the change takes full effect on the next navigation. Set it before navigation, or reload after changing it. Puppeteer: setJavaScriptEnabled.

2. Choose a readiness signal that matches the page

Browser lifecycle events describe stages of navigation, not whether the specific content in your screenshot is ready. Prefer the narrowest condition that proves the needed content exists.

Signal What it tells you When to use it Limitation
commit The response has started and the document navigation has committed. Early navigation work when you will wait for a separate condition. Content may not yet be parsed or rendered.
domcontentloaded The document has been parsed and the DOMContentLoaded event fired. A quick navigation milestone before waiting for app content. Client-side rendering and data requests may still be underway.
load The page load event has fired. Pages whose required resources are covered by the load event. Applications can continue rendering or fetching data afterward.
networkidle or Puppeteer’s networkidle2 Network activity has fallen below the respective tool’s threshold for a period. A diagnostic or page-specific heuristic when network activity settles. Analytics, polling, streaming, and other requests can make it slow or misleading.
Target selector, text, or app state The particular marker your capture depends on is present. Recommended when the page has a stable readiness marker. The marker must be stable and actually represent the desired visual state.

Playwright lists commit, domcontentloaded, load, and networkidle as navigation signals, but explicitly discourages using networkidle as a general testing readiness criterion. Its guidance is to rely on assertions about the page instead. The same practical distinction applies to screenshot automation: wait for what must appear in the image. Playwright Page API: navigation waits.

Wait for a selector or expected text

When the page exposes a meaningful element, wait for it to become visible. For a known text marker, wait for that text or check it with a page-specific condition. Set a timeout so an absent marker becomes a clear failure instead of an indefinite wait.

await page.waitForSelector('main article h1', {
  visible: true,
  timeout: 15000,
});

const heading = await page.$eval('main article h1', el => el.textContent.trim());
if (!heading) throw new Error('The article heading is empty');

Wait for an application-specific condition

If the page has a reliable global state or data marker, use it. Puppeteer provides waitForFunction; the condition below is an example only. Adapt it to a state owned by the target application rather than guessing at internal variables.

await page.waitForFunction(
  () => document.querySelector('[data-page-ready="true"]') !== null,
  { timeout: 15000 }
);

Use fixed delays only as a fallback

A delay can help confirm that a page is merely slow, but it is fragile: a short delay fails on slow runs, while a long delay wastes time on fast runs. If no reliable signal exists, use a bounded delay after navigation and validate the captured output. Puppeteer also supports waiting for elements and functions; consult the documentation for the version installed in your project. Puppeteer Page API.

3. Handle frames, lazy content, and interaction

Content inside an iframe

A selector in the main document does not establish that an embedded frame has finished rendering. Identify the frame that contains the target, then wait for its own marker before capturing. Puppeteer documents frame waiting and page functions as tools for dealing with page state; frame selection depends on the target site.

const frame = await page.waitForFrame(
  candidate => candidate.url().includes('/embedded-content'),
  { timeout: 15000 }
);

await frame.waitForSelector('.report-ready', {
  visible: true,
  timeout: 15000,
});

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

Replace /embedded-content and .report-ready with the actual frame URL pattern and target element. If the frame is cross-origin, browser automation can still interact with its frame context, but page JavaScript running in the parent should not be used to read cross-origin frame internals.

Lazy-loaded images or content that requires scrolling

Some pages load images only when they approach the viewport. If the screenshot needs content below the fold, reproduce the trigger the site expects—often scrolling—and then wait for the images or content to appear. Do not assume a generic screenshot option forces every site’s lazy-loading behavior. Check the page’s own behavior and verify the resulting image.

await page.evaluate(async () => {
  const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
  for (let y = 0; y < document.body.scrollHeight; y += step) {
    window.scrollTo(0, y);
    await new Promise(resolve => setTimeout(resolve, 150));
  }
  window.scrollTo(0, 0);
});

await page.waitForSelector('img.article-hero', { visible: true });
await page.screenshot({ path: 'full-page.png', fullPage: true });

The short scroll pause is site-dependent and is not a universal loading guarantee. For stronger checks, wait for specific images to complete (for example, check that the relevant image elements have loaded) or for the page’s own ready state.

Content revealed by a click

If the desired state appears only after an interaction, perform that interaction and wait for its result. For example, after clicking a tab, wait for the associated panel to become visible before capture. Make sure the interaction is safe to repeat and does not trigger a destructive action.

4. Keep screenshots repeatable

For visual comparisons, keep the rendering environment consistent: browser version, operating system, headless or headful mode, viewport, device scale factor, fonts, and other relevant settings. Playwright notes that host OS, browser version, settings, hardware, power source, and headless mode can affect rendering, and recommends taking screenshots in the same environment as the baseline. Playwright visual comparisons.

  • Use the same browser build and operating system for baseline and new captures.
  • Set viewport dimensions and device scale factor explicitly.
  • Wait for animations or dynamic content to settle if they affect the comparison.
  • Use the same authentication, locale, timezone, and page state each run.
  • Capture only after the content marker is present, and use the same timeout policy.

5. Troubleshoot blank or incomplete screenshots

Symptom Likely cause Fix
Entire page is blank JavaScript is disabled, navigation did not complete, or the page failed before rendering. Enable JavaScript before navigation, inspect navigation errors, and wait for a real page marker.
HTML shell appears, but app content is missing The initial document loaded before client-side rendering or data requests finished. Wait for a visible target element, expected text, or app-specific ready state.
Wait for selector times out The selector is wrong, the element is hidden, it is in a frame, or the page never reached that state. Confirm the selector in the rendered page, check visibility, identify the correct frame, and inspect the page before increasing the timeout.
networkidle wait hangs Long polling, analytics, streaming, or recurring requests keep the network active. Use a selector or application assertion instead of requiring global network silence.
Only the top section is complete Lower content or images load on scroll, or the full-page capture began before those triggers. Scroll through the relevant area, wait for target content, and verify the full-page result.
Embedded section is blank The target content belongs to an iframe that has not rendered or was not queried. Wait for the correct frame and its own readiness marker.
Screenshots differ between runs Browser, OS, viewport, device scale, fonts, headless mode, or dynamic page content changed. Pin the rendering environment and stabilize the page state before capture.
Changing JavaScript setting has no effect The setting change applies on the next navigation. Set it before navigation, or reload after changing it.

6. Performance, reliability, and cost

A targeted readiness condition usually avoids waiting for a long arbitrary delay, while still proving the required content is present. Use explicit timeouts to bound slow or broken pages, and record whether the failure occurred during navigation or while waiting for the page-specific marker. Network idle can be quick on some pages and unhelpful on pages with ongoing requests, so choose it only when that behavior fits the site.

Browser automation gives control over the environment and interactions, but you need to manage the browser runtime, dependencies, execution time, and failure handling. Keep browser versions aligned for repeatability. If captures run at scale, account for browser startup and page load time in your job design, and avoid retrying deterministic failures without changing the cause. No universal timing or cost benchmark applies; both depend on the site, runtime, and capture setup.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Its capture options include waiting for a selector, a delay, or network idle. 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}`);
const fs = require('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for free and capture 1,000 screenshots a month with no card.

FAQ

Does a screenshot tool execute JavaScript automatically?

A browser-based capture can execute page JavaScript, but the capture still needs to wait for the page state you want. Check the tool’s JavaScript and wait settings.

Is waiting for load enough?

Only when the content you need is ready by that event. Client-rendered apps often need an additional selector, text, or application-state check.

Should I always wait for network idle?

No. Persistent requests can prevent it from occurring, and network silence does not prove the target element is visible. Prefer a condition tied to the content in the screenshot.

Can a fixed delay solve the problem?

It can be a temporary diagnostic or last resort, but it is less reliable than waiting for an observable page condition.