Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →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:
- Navigate to the target page with
page.goto(). - Inject the remote JavaScript URL with
await page.addScriptTag({ url: scriptUrl }). - Wait for the script’s actual result, such as a selector, attribute, network response, or application flag.
- Capture with
page.screenshot(), addingfullPage: truewhen 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.
Recommended Free Tools
#1 Best Overall
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:
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
Rank #2
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:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteawait 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: trueto capture the complete scrollable page; omit it for the visible viewport only. - Format: Use a filename ending in
.png,.jpeg, or.webpas 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:
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.
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.
Rank #4
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.
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.
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.
Best Value
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.
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.
Quick Recap
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.

