How to Inject JavaScript Before Capturing a Webpage
Register a new-document script before navigation, then wait for the page state your screenshot needs. See working Playwright, Puppeteer, and Chrome DevTools Protocol examples.

To run JavaScript before a webpage’s own scripts and then capture it, register a new-document initialization script before navigating. In Playwright, use page.addInitScript() for one page or browserContext.addInitScript() for pages in a context. Navigate, wait for the state your image needs, and call page.screenshot(). For Puppeteer, use page.evaluateOnNewDocument(); for direct Chrome DevTools Protocol access, use Page.addScriptToEvaluateOnNewDocument. These APIs run code in a new document before the page’s own scripts. Playwright Page API, Playwright BrowserContext API, Puppeteer Page API, and the Chrome DevTools Protocol Page domain document these approaches.
1. The reliable sequence
- Choose the browser automation library or protocol you already use.
- Create the page or browser context.
- Register the initialization script before navigation.
- Navigate to the target URL.
- Wait for the page state that matters to your capture.
- Capture the viewport or full page and close the browser cleanly.
The order matters. A script tag added after navigation is a normal page insertion; the page may already have executed its own JavaScript. A new-document hook is designed for code that must run before those scripts. The hook is not itself a screenshot-readiness signal: navigation completion does not guarantee that an app has rendered the particular data, animation, or asynchronous content you need.

2. Playwright: complete JavaScript example
Install Playwright and its browser using the documented installation steps for your environment. The following CommonJS script writes a viewport screenshot to page.png. Replace the flag-setting code with the initialization your capture requires.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1
});
const page = await context.newPage();
// Runs after document creation and before page scripts.
await page.addInitScript(() => {
window.captureFlag = true;
});
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Replace with a selector that represents the state you need.
await page.locator('body').waitFor({ state: 'visible', timeout: 10000 });
await page.screenshot({ path: 'page.png' });
await context.close();
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The injection point must precede page.goto(). In this example, the body visibility wait is a minimal placeholder, not a guarantee that a client-rendered application has finished. Prefer a site-specific condition such as a result container becoming visible or a loading indicator disappearing. Playwright documents page.addInitScript() as applying on navigation and when child frames attach or navigate. The callback can be a function or script content; an optional serializable argument can be passed to a function. A file can also be registered with the path option.
Page scope or context scope?
Use page scope when a hook belongs to one tab. Use context.addInitScript() when new pages and navigations in the context should receive the setup. For example, register on the context immediately after creating it and before opening pages or navigating. Both scopes are useful when testing popups, multiple tabs, or frames. Playwright warns that the order of multiple page-level and context-level init scripts is undefined. If one script depends on another, combine them into a single initialization or make them independent.
Viewport, full-page, and element captures
The example captures the current viewport. For a full-page image, use await page.screenshot({ path: 'page.png', fullPage: true });. For a particular element, capture its locator, for example await page.locator('main').screenshot({ path: 'main.png' });. Before a full-page capture, account for lazy-loaded images: scroll through the page or use an application-specific readiness condition so content below the fold has had a chance to load. A very tall page can produce a large image, so choose full-page capture only when the full document is required.
3. Puppeteer: inject before navigation
Puppeteer’s documented pre-page-script mechanism is page.evaluateOnNewDocument(). Register it before goto(), then wait for the content you care about and call screenshot().
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000 });
await page.evaluateOnNewDocument(() => {
window.captureFlag = true;
});
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForSelector('body', { visible: true, timeout: 10000 });
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
As with Playwright, select a wait condition based on the page, not habit. Puppeteer and Playwright expose different APIs, but the workflow is the same: install the hook, navigate, wait for the relevant state, capture. Consult the official API reference linked above for current options in the version you use.
4. Direct Chrome DevTools Protocol
If your automation already manages a Chrome DevTools Protocol session, the Page domain has Page.addScriptToEvaluateOnNewDocument for scripts evaluated in every frame upon creation before that frame’s scripts. The corresponding screenshot method is Page.captureScreenshot. You must enable the Page domain and send protocol commands through an established CDP connection. This route makes sense when you need direct protocol access; when working in Playwright or Puppeteer, their page screenshot APIs usually keep the surrounding browser workflow simpler.
// Pseudocode: send these commands over an existing CDP session.
await cdp.send('Page.enable');
await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
source: 'window.captureFlag = true;'
});
await cdp.send('Page.navigate', { url: 'https://example.com' });
// Wait for the page state required by your application here.
const result = await cdp.send('Page.captureScreenshot', {
format: 'png'
});
// Decode result.data from base64 and save it as a PNG file.
This is a protocol sketch rather than a standalone runnable program: creating a CDP connection and saving the returned base64 data depends on the client you use. The protocol reference defines the commands; use your client’s documentation for session setup and command invocation.
5. Choose the right hook and readiness condition
| Need | Use | Key detail |
|---|---|---|
| One Playwright page | page.addInitScript() |
Register before navigation; applies to new documents and child frames. |
| Pages in a Playwright context | browserContext.addInitScript() |
Register at context level for shared setup across pages. |
| Puppeteer page | page.evaluateOnNewDocument() |
Install before navigating. |
| Direct Chrome protocol client | Page.addScriptToEvaluateOnNewDocument |
Use with a CDP session; capture with Page.captureScreenshot. |
There is no universal readiness event that ensures every site is ready for every screenshot. Pick the narrowest signal that matches the desired output:
- Static or server-rendered page: document creation or DOM readiness may be sufficient.
- Client-rendered content: wait for a visible, meaningful selector or a known application state.
- Images and fonts: wait for the relevant assets or application signal if they affect the image.
- Animations: disable them for deterministic output or wait until the desired frame; account for the fact that animation timing varies.
- Lazy content: scroll to trigger loading before taking a full-page capture.
6. What to initialize, and what not to assume
New-document scripts are useful for setting a flag the page reads, seeding a controlled test environment, or installing a small shim before application code starts. Keep initialization deterministic and safe to execute again: navigations and child-frame creation can cause it to run multiple times. Guard against duplicate listeners or repeated mutations where necessary.
Code registered this way runs in each new document’s JavaScript context. Do not assume variables created in one navigation survive the next. Do not assume a main-frame script automatically coordinates with a child frame: the API may execute in the new frame, but frame-specific origins, application behavior, and access restrictions still matter. If the script depends on a value from Node.js, pass only serializable data using the supported argument mechanism rather than expecting the browser callback to close over local Node variables.
Likewise, page.addScriptTag() and script-tag insertion are useful when adding code to an already loaded document, but they do not meet the requirement that code precede the page’s own scripts. Use them only when late injection is acceptable. Avoid overriding browser primitives broadly unless the page or test specifically requires it; such changes can affect unrelated application code and make a capture harder to interpret.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Page code cannot see the injected value | The hook was registered after navigation, or the code ran in a different document/frame. | Register before goto(); confirm whether the target code runs in the main frame or a child frame. |
| Injection appears to run too late | A script tag was inserted after the page started executing. | Use the framework’s new-document API before navigation. |
| Screenshot is blank or partially rendered | The capture happened before application content appeared, or navigation reached a minimal document state only. | Wait for a page-specific selector or app-ready condition and inspect navigation errors. |
| Works on first load but not after a redirect or reload | Initialization was registered in the wrong place or after the new document began. | Register the hook once before navigation; new-document hooks apply again on navigation. |
| One of several initialization scripts sees missing state | Playwright does not define the order between page and context init scripts. | Combine dependent work in one script or remove the dependency. |
| Screenshot misses an iframe’s content | The frame has not loaded, or the readiness check only covers the main document. | Wait for a frame-specific condition when required; verify the frame is present and rendered. |
| Timeout while waiting | The selector never appears, the page is blocked, or the wait does not match its rendering behavior. | Check the selector and page state, set a deliberate timeout, and log navigation or console errors for diagnosis. |
| Full-page image omits lower images | Images load lazily only when brought near the viewport. | Scroll the page to trigger loading before the full-page capture, then wait for the assets you need. |
8. Performance, reliability, and cost
A small initialization script adds little work compared with launching a browser and loading a site, but its actual cost depends on the script and target page. Avoid expensive loops, repeated DOM scans, or long-running tasks in a hook that executes for every new frame or navigation. Keep shared context setup small, and add page-specific logic only where needed.
For reliability, create a fresh context when isolation matters, set a deliberate viewport, use a page-specific readiness condition, and close contexts and browsers in a finally block. If captures are concurrent, bound concurrency to the memory and CPU available to the machine; no universal throughput figure applies across sites and environments. Retries can help with transient navigation failures, but retry only the failed operation and avoid capturing a different page state silently.
Self-hosted automation costs include the compute and maintenance needed to run browsers, plus engineering time for navigation, readiness, storage, retries, and debugging. The supplied framework references do not establish comparative performance, infrastructure prices, or a universal cost per screenshot. Measure your own representative URLs and include failures and retries when estimating spend.
9. Or skip the browser setup
If you need a screenshot rather than control over a custom pre-page script, ScreenshotNeo is a website screenshot API and MCP server for developers. It accepts one GET request with a URL and returns an image or PDF. Its documented product facts include cookie and consent banners, newsletter popups, and chat widgets removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report page verdict and billing status in headers. The MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients. Free includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. 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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
These are one-call screenshot examples, not a way to run an arbitrary custom initialization callback before the target site’s scripts. Use browser automation when that specific control is required. Choose the output and capture configuration that fits your task using the API documentation. Create a free account to get 1,000 screenshots a month with no card.
10. Frequently asked questions
Does “before page scripts” mean before the browser creates the document?
No. Playwright documents the hook as running after document creation but before that document’s scripts.
Will the init script run again after a reload?
New-document hooks are designed to run for navigations, so expect initialization to run again and make it safe to repeat.
Can I inject JavaScript into a page already open?
Yes, with a page evaluation or script insertion API, but that is late injection and cannot guarantee execution before scripts that already ran.
Should I wait for network idle before capturing?
Only if that condition matches the site and the state you want. A page can keep network activity open, or become visually ready before all requests stop; a specific UI condition is often clearer.
Can ScreenshotNeo run my custom init script?
The API call shown here captures a URL. For custom JavaScript that must run before the page’s scripts, use a browser automation or CDP workflow that exposes the relevant new-document hook.


