Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Playwright’s page.addScriptTag({ url }), await the returned promise, wait for the specific page state your script creates, and then call page.screenshot(). The promise confirms that the remote script’s load event fired; it does not prove that timers, network requests, or rendering started by that script have finished. Capture only after the required effect is observable.

The reliable sequence

A robust capture flow has four stages:

  1. Navigate to the target page with page.goto().
  2. Inject the remote JavaScript URL with await page.addScriptTag({ url: scriptUrl }).
  3. Wait for the script’s actual result, such as a selector, attribute, network response, or application flag.
  4. Capture with page.screenshot(), adding fullPage: true when the entire scrollable document is required.

Playwright’s documented addScriptTag operation adds a <script> element using a URL or inline content. Awaiting it waits for that element’s load event. It does not wait for asynchronous work that the loaded file starts later.

Complete Playwright example

The following Node.js script loads a remote file, waits for a page-specific marker, and saves a full-page PNG. Replace both URLs and the readiness condition with values for your site.

import { chromium } from 'playwright';

const targetUrl = 'https://example.com';
const scriptUrl = 'https://cdn.example.com/widget.js';

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: 1440, height: 900 }
});

try {
  await page.goto(targetUrl, { waitUntil: 'load', timeout: 60_000 });

  await page.addScriptTag({ url: scriptUrl });

  // Replace this with the effect your script produces.
  await page.waitForSelector('[data-widget-ready="true"]', {
    state: 'visible',
    timeout: 30_000
  });

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

page.goto() waits for the navigation load event by default. That event includes dependent resources such as stylesheets, scripts, iframes, and images, but modern applications often continue fetching data and updating the interface afterward. The selector wait is therefore the important boundary for this example.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choosing the right readiness condition

Wait for a selector

Use page.waitForSelector() when the injected code adds an element or changes its visibility. A dedicated attribute such as data-widget-ready="true" is less fragile than waiting for a generic class.

await page.waitForSelector('#results[data-loaded="true"]', {
  timeout: 30_000
});

Wait for an attribute or text change

When an element already exists, wait until its state changes.

await page.waitForFunction(() => {
  const el = document.querySelector('#status');
  return el?.textContent?.includes('Complete');
});

Wait for a known network response

If the script fetches a predictable API resource, wait for that response before capturing.

await page.waitForResponse(response =>
  response.url().includes('/api/data') && response.ok()
);

Start the response wait before the action that triggers the request so a fast response cannot be missed:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const dataResponse = page.waitForResponse(r =>
  r.url().includes('/api/data') && r.ok()
);
await page.addScriptTag({ url: scriptUrl });
await dataResponse;

Use a fixed delay only when necessary

page.waitForTimeout(1000) can be useful for a known animation or debounce, but it is not proof that work is complete. Prefer a condition tied to the result you need. If no observable signal exists, combine a conservative delay with a check of the rendered state.

Remote script loading details

Cross-origin URLs

A script element can load a file from another origin when the URL is reachable by the browser. The remote server must return JavaScript that the browser accepts, and content-security policy, authentication, redirects, or certificate errors can still block it.

Execution context

addScriptTag injects the file into the current page. It runs in that page’s normal document context, so it can read and modify DOM state available to page scripts. If the script depends on a particular frame, inject it into that frame rather than the top-level page.

const frame = page.frames().find(f => f.url().includes('app.example.com'));
if (!frame) throw new Error('Target frame was not found');
await frame.addScriptTag({ url: scriptUrl });

Script dependencies

If the file expects a library to be loaded first, add dependencies in order and await each load:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.addScriptTag({ url: 'https://cdn.example.com/library.js' });
await page.addScriptTag({ url: 'https://cdn.example.com/plugin.js' });

Loading a file successfully does not guarantee that its global object exists if the file exits early. Check the expected API or page state before taking the screenshot.

When code must run before the page’s own scripts

Use page.addInitScript() when initialization must happen before site scripts execute—for example, defining a browser API shim or setting an initial value that application code reads during startup. Its documented inputs are inline content or a local file path. It is not the direct remote-URL equivalent of addScriptTag({ url }).

await page.addInitScript({
  content: `Object.defineProperty(navigator, 'language', {
    get: () => 'en-US'
  });`
});
await page.goto(targetUrl);

Install the initialization script before navigation. Ordering between multiple context-level and page-level initialization scripts is undefined, so put dependent setup in one script when order matters.

Capture options that affect the result

  • Viewport: Set a deterministic width and height with browser.newPage({ viewport }).
  • Full page: Set fullPage: true to capture the complete scrollable page; omit it for the visible viewport only.
  • Format: Use a filename ending in .png, .jpeg, or .webp as supported by your Playwright version.
  • Animations: Disable or wait for animations if visual consistency matters.
  • Lazy content: Scroll or trigger the page’s lazy-loading condition before capture when content is not present in the DOM yet.
await page.screenshot({
  path: 'page.webp',
  type: 'webp',
  quality: 85,
  fullPage: true
});

Common failures and fixes

“Script failed to load”

Check the URL from the browser context, redirects, TLS certificates, DNS, and content-security policy. Open the URL in the same environment and listen for page errors:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.on('pageerror', error => console.error('Page error:', error));
page.on('console', message => console.log(message.type(), message.text()));

The screenshot is taken too early

The script’s load event fired, but its later fetch or timer has not completed. Replace a fixed delay with a selector, attribute, response, or application flag that represents the finished state.

The expected element never appears

Verify that the script actually ran in the intended frame, that it did not require a missing dependency, and that the selector matches the post-script DOM. Capture the HTML or inspect the console while diagnosing.

Navigation times out

Raise the navigation timeout only when the page is legitimately slow, and choose a less strict navigation milestone if appropriate. Do not treat a timeout increase as proof that the page is ready.

Authentication or cookies are missing

Create a browser context with the required storage state, cookies, or headers before navigation. A remote script may also call an API that requires credentials, so confirm those requests in the page context.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only part of the page appears

Use fullPage: true. If the page loads content while scrolling, trigger its lazy-loading behavior first and wait for the resulting elements before capturing.

Performance, reliability, and repeatability

Keep one browser instance alive for batches of captures and create isolated contexts or pages per target. Reuse a context only when sharing cookies and local storage is intentional. Set explicit timeouts, log the target URL and script URL, and save diagnostics such as console errors and a screenshot on failure.

For repeatable images, fix the viewport, timezone, locale, color scheme, and device scale factor. Wait on semantic readiness rather than elapsed time, and ensure the remote script is version-pinned when the provider offers immutable URLs. A script URL can change without your capture code changing.

Do not assume that navigation’s load event means every framework task is finished. There is no universal definition of a completely “loaded” modern page; the correct boundary depends on the page and the state your image must show.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a single-call website screenshot API and an MCP server for developers and AI agents. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled.

Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options, including full-page capture, CSS-selector elements, dark mode, device presets, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.

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(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Does addScriptTag wait for promises inside the script?

No. It waits for the script element’s load event. Wait separately for the DOM or network state produced by asynchronous code.

Can I inject JavaScript before navigation with a URL?

For pre-navigation initialization, use addInitScript with inline content or a local file. For a remote URL in an already navigated document, use addScriptTag({ url }).

Should I always use a full-page screenshot?

No. Use fullPage: true only when the complete scrollable document is needed; viewport captures are faster and more predictable for above-the-fold checks.

Frequently Asked Questions

Does addScriptTag wait for promises inside the script?

No. It waits for the script element’s load event. Wait separately for the DOM or network state produced by asynchronous code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I inject JavaScript before navigation with a URL?

For pre-navigation initialization, use addInitScript with inline content or a local file. For a remote URL in an already navigated document, use addScriptTag({ url }).

Should I always use a full-page screenshot?

No. Use fullPage: true only when the complete scrollable document is needed; viewport captures are faster and more predictable for above-the-fold checks.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.