ScreenshotNeo

BlogHow-to

How to Run JavaScript Before a Page Loads in Puppeteer

Use Puppeteer’s `page.evaluateOnNewDocument()` to register JavaScript before a document’s scripts run. Learn how to pass data, cover frames, remove a hook, and troubleshoot common mistakes.

By the ScreenshotNeo team4 October 20266 min read

To run JavaScript before a page’s scripts execute in Puppeteer, register it with page.evaluateOnNewDocument() before calling page.goto(). Puppeteer runs the function after a document is created and before that document’s scripts run.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();

  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'languages', {
      get: () => ['en-US', 'en'],
    });
  });

  await page.goto('https://example.com');
  console.log(await page.evaluate(() => navigator.languages));
} finally {
  await browser.close();
}

This is different from page.evaluate(), which evaluates code in the current page context, and page.addScriptTag(), which inserts a script element. Use the document-start hook when page scripts must see a value or setup from the beginning. See the official Puppeteer API reference.

1. Install Puppeteer and run the example

In a new project, install Puppeteer and save the following as before-load.mjs:

npm install puppeteer
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();

  // Register this before navigation. The function executes in the page.
  await page.evaluateOnNewDocument(() => {
    window.myEarlySetup = 'ready';
  });

  const response = await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded',
  });

  console.log('HTTP status:', response?.status() ?? 'no main resource response');
  console.log('Setup value:', await page.evaluate(() => window.myEarlySetup));
} finally {
  await browser.close();
}
node before-load.mjs

The callback is serialized and runs in the browser page context. It does not share Node.js lexical scope, so keep it self-contained or pass values explicitly as arguments.

2. Pass values from Node.js

Pass serializable values after the callback. This avoids relying on variables that exist only in the Node.js process.

const settings = {
  languageList: ['en-GB', 'en'],
  flag: 'setup-from-node',
};

await page.evaluateOnNewDocument((config) => {
  Object.defineProperty(navigator, 'languages', {
    get: () => config.languageList,
  });
  window.setupFlag = config.flag;
}, settings);

await page.goto('https://example.com');
console.log(await page.evaluate(() => ({
  languages: navigator.languages,
  flag: window.setupFlag,
})));

Do not expect this to work:

const value = 'from Node';
await page.evaluateOnNewDocument(() => {
  window.example = value; // ReferenceError: value is not in the page context.
});

Pass value as an argument instead. Page-evaluation return values are serialized back to Node.js; if you need to retain a DOM object by reference, use a handle API rather than expecting a serialized object to preserve that reference.

3. Choose the right script-injection API

API When it runs Use it for
page.evaluateOnNewDocument(fn, ...args) On future document creation, before that document’s scripts Setup that must be present when the page’s own scripts begin
page.evaluate(fn, ...args) When called, in the current page context Reading or changing the current page after it exists
page.addScriptTag({content}) or a URL option By adding a script element to the page Loading script content or a script URL into the main frame

page.evaluate() awaits a promise returned by the page function. It is not the documented registration hook for running before a future document’s scripts. page.addScriptTag() inserts a tag and is documented as a shortcut for the main frame; it does not provide the same document-start registration semantics.

4. Understand navigation, frames, and hook lifetime

Register before the navigation you care about

Call evaluateOnNewDocument() before goto() or another navigation that creates the target document. Registering after a page has loaded does not retroactively run the function in that already-created document.

It runs again for new documents

Puppeteer documents invocation on navigation and when child frames attach or navigate. Treat the callback as repeatable: avoid Node-side assumptions about it running only once, and make page-side changes safe if repeated.

Frame scope needs care

The API reference describes invocation for child-frame attachment and navigation. However, Puppeteer’s Frame reference says evaluation in one frame does not affect nested child frames. Do not assume that evaluating in the main frame changes every iframe’s JavaScript environment. If iframe behavior matters, inspect the specific frames and verify the result in the target page.

Remove a registered hook

The registration returns an identifier. Keep it if you may need to stop injecting the script on subsequent documents:

const registration = await page.evaluateOnNewDocument(() => {
  window.earlyFlag = true;
});

// Later: stop applying this registered script to future documents.
await page.removeScriptToEvaluateOnNewDocument(registration.identifier);

Removing the registration does not undo changes already made in the current document; it prevents future injections from that registration.

5. Navigate and choose when Puppeteer waits

page.goto(url, options) returns the main resource response, or null for cases such as about:blank and same-URL hash navigation. Its waitUntil option controls when navigation waiting resolves. Select the event that matches the task: for example, domcontentloaded when the document has been parsed, or load when the load event is the required boundary. A navigation event is not proof that every application request or delayed widget has finished.

const response = await page.goto('https://example.com', {
  waitUntil: 'load',
  timeout: 30_000,
});

if (response) {
  console.log(response.status(), response.url());
} else {
  console.log('Navigation did not provide a main resource response.');
}

See the official Puppeteer page.goto() reference for current navigation options.

6. Troubleshooting

Symptom Likely cause Fix
The page’s first script cannot see the value The hook was registered after navigation, or the code was run with page.evaluate() after load. Register with evaluateOnNewDocument() before navigating to the document.
ReferenceError for a Node variable The callback runs in the browser context and does not close over Node.js variables. Pass the data as an argument: page.evaluateOnNewDocument((value) => { ... }, value).
Setup appears to happen multiple times A new document was created by navigation, reload, or frame activity. Expect reinvocation for new documents and make the page-side setup safe to repeat.
An iframe does not show the expected change Frame execution scope differs; main-frame evaluation does not automatically modify nested frame contexts. Inspect the relevant frame and verify the behavior required for it instead of assuming main-frame changes propagate.
page.goto() returns null Some navigations, including about:blank and same-URL hash navigation, have no main resource response. Handle a null response rather than calling response methods unconditionally.
Navigation times out even though the hook ran The page did not reach the selected navigation wait condition within the timeout. Choose a suitable waitUntil condition and timeout for the page; diagnose navigation separately from script registration.
Changes vanish after a redirect or reload A new document replaced the old one, or the registration was removed. Keep the registration active before navigation; the registered hook is invoked for new documents.

7. Performance, reliability, and cost

The API documentation establishes when the callback runs, but does not provide a performance benchmark or a cost model. Keep the hook small: it executes in each applicable new document, so expensive work can delay page scripts. Avoid network calls or lengthy loops in document-start setup unless the page specifically requires them.

For reliability, register once at the point where the page is created, pass explicit data, make the setup repeatable, and test pages with redirects, reloads, and relevant iframe structures. Choose navigation waits based on what the next operation needs, not as a substitute for confirming that the desired page state exists. Browser hosting and execution cost depend on where and how Puppeteer is run; the Puppeteer API sources do not establish a general price.

8. Or skip the browser setup

If your goal is to capture a page rather than customize its JavaScript environment, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A single GET request can return a PNG, JPEG, WebP, or PDF. This example saves a WebP response:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for the API options. Cookie banners are accepted and removed along with 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, with the page verdict and billing status reported in response headers. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card required.

9. FAQ

Does the hook run before the browser creates the document?

No. Puppeteer documents it as running after document creation but before the document’s scripts.

Can I use this to change the page’s response HTML?

This API evaluates JavaScript in the page context; it is not an HTML response-rewriting API. Use it to establish page-side state or behavior.

Can I stop the hook after one navigation?

Yes. Store the registration identifier and pass it to removeScriptToEvaluateOnNewDocument() when you want to prevent later injections.

Does this run in Node.js?

No. The callback runs in the browser page context. Pass values from Node.js as arguments, and return serializable results when you need data back.