ScreenshotNeo

BlogHow-to

How to Use Web Workers with Puppeteer

Learn how to detect dedicated Web Workers, inspect current workers, run code in a worker context, and handle evaluation results safely with Puppeteer.

By the ScreenshotNeo team4 October 20268 min read

Puppeteer exposes page-associated dedicated Web Workers as WebWorker objects. Attach workercreated and workerdestroyed listeners to a Page to follow their lifecycle, call page.workers() to get the dedicated workers associated with the page at that moment, and use worker.evaluate() or worker.evaluateHandle() to run code in a worker context. page.workers() does not include ServiceWorkers. See the official WebWorker API and Page.workers() reference.

1. Install Puppeteer and create a page

This JavaScript example uses ECMAScript modules and a recent Puppeteer release. Install Puppeteer in a Node.js project, save the program as workers.mjs, then run it with Node. Puppeteer’s installed version determines the exact API signatures; consult its documentation if you use a different release.

npm install puppeteer
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  // Register worker listeners before navigation in the next step.
} finally {
  await browser.close();
}

2. Listen for workers before navigation

Register listeners before page.goto() if you need to observe workers created during initial loading. The callbacks receive a worker object; its url() method identifies the worker script URL.

import puppeteer from 'puppeteer';

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

  page.on('workercreated', worker => {
    console.log('Worker created:', worker.url());
  });

  page.on('workerdestroyed', worker => {
    console.log('Worker destroyed:', worker.url());
  });

  await page.goto('https://example.com');
  console.log('Current dedicated workers:', page.workers().map(worker => worker.url()));
} finally {
  await browser.close();
}

The official Page event reference names workercreated and workerdestroyed as worker lifecycle events. A page only has workers if its own scripts create them; the events and snapshot method do not cause a site to start a worker. Check the PageEvent reference for the event names supported by your installed Puppeteer version.

3. Get a snapshot of current dedicated workers

Use page.workers() when you want the workers currently associated with the page rather than lifecycle notifications. It returns an array of dedicated WebWorker objects. A snapshot does not include workers that are created later, so combine it with lifecycle listeners if both existing and future workers matter.

const currentWorkers = page.workers();

for (const worker of currentWorkers) {
  console.log(worker.url());
}

Do not treat this as a browser-wide inventory. In particular, Puppeteer documents that the method excludes ServiceWorkers.

4. Evaluate code in a worker

worker.evaluate() runs a function in the worker context. It awaits a promise returned by that function, making it suitable for asynchronous worker-side checks. Keep arguments and return values simple and serializable where possible.

for (const worker of page.workers()) {
  const details = await worker.evaluate(() => ({
    href: self.location.href,
    hasNavigator: typeof navigator !== 'undefined',
  }));

  console.log(details);
}

Worker code does not share the page’s window global. Use worker globals such as self and access only APIs available in that worker environment. A function passed to evaluate() is evaluated in the target context, so do not rely on lexical variables from the Node.js process unless you pass them as arguments.

Pass data explicitly as serializable arguments:

const result = await worker.evaluate((prefix, values) => {
  return {
    label: `${prefix}:${self.location.pathname}`,
    count: values.length,
  };
}, 'worker', [1, 2, 3]);

5. Choose between evaluate and evaluateHandle

Use evaluate() for results that can be transferred back as ordinary values such as strings, numbers, arrays, and plain objects. Browser protocol serialization can make complex return values incomplete or represent them as {}. When the value cannot be usefully serialized or you need to retain a live object reference, use evaluateHandle(). These methods are documented on the WebWorker.evaluate API page and the WebWorker class reference.

const handle = await worker.evaluateHandle(() => ({
  location: self.location,
  nested: { ready: true },
}));

try {
  // Keep the handle for operations supported by the installed Puppeteer release.
  console.log('Received a worker object handle');
} finally {
  await handle.dispose();
}

Dispose of handles when you are finished with them. A handle represents a browser-side object; it is not a plain JavaScript object that can always be inspected or serialized in Node as-is.

6. Wait for a condition inside a worker

worker.waitForFunction() waits until a function evaluated in that worker returns a truthy value. The API accepts options including polling, timeout, and an abort signal; consult the installed release’s WebWorker reference for its current signature and defaults.

await worker.waitForFunction(() => self.location.href.length > 0, {
  timeout: 10_000,
});

const href = await worker.evaluate(() => self.location.href);
console.log(href);

Use a finite timeout when a condition might never become true. For conditions tied to application messages or state, ensure the worker code actually exposes a condition that can be checked in its global context.

7. Complete example: capture lifecycle and inspect workers

This combines event listeners, a current-worker snapshot, evaluation, and cleanup. The target page may or may not create a worker; the example reports what the page exposes.

import puppeteer from 'puppeteer';

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

  page.on('workercreated', worker => {
    console.log('Worker created:', worker.url());
  });
  page.on('workerdestroyed', worker => {
    console.log('Worker destroyed:', worker.url());
  });

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

  for (const worker of page.workers()) {
    const info = await worker.evaluate(() => ({
      href: self.location.href,
      hasNavigator: typeof navigator !== 'undefined',
    }));
    console.log('Worker info:', info);
  }
} finally {
  await browser.close();
}

The waitUntil setting controls page navigation waiting; it is separate from waiting for a worker condition. If a worker is created after the snapshot, the event listener can still report its creation, but that later worker will not be part of the array already returned by page.workers().

8. Dedicated WebWorkers and ServiceWorkers

Puppeteer’s page.workers() is for dedicated WebWorkers associated with a page. It explicitly does not return ServiceWorkers. Do not describe it as returning every worker in a browser context. ServiceWorker inspection uses different target concepts and APIs; the sources cited here do not establish a complete ServiceWorker workflow, so verify the target and browser-context APIs for the specific Puppeteer version before implementing one.

Similarly, page.evaluateOnNewDocument() injects code into page documents after document creation and before page scripts. Its documentation covers navigations and child frames, but does not say it runs in worker contexts. Do not use it as a worker preload hook without a separately documented mechanism. See the evaluateOnNewDocument reference.

9. Troubleshooting

Symptom Likely cause Fix
No workercreated event appears The page may not create a dedicated worker, or the listener was attached after creation. Attach the listener before navigation and inspect page.workers() after navigation for the current snapshot. The site determines whether workers exist.
page.workers() is empty No dedicated workers are currently associated with that page, or the worker was created in a different context. Confirm the page actually starts a dedicated worker; use lifecycle events to observe future creation. Remember that ServiceWorkers are excluded.
A value from worker.evaluate() is missing fields or becomes {} The returned object may not serialize cleanly through the browser protocol. Return primitives and plain JSON-shaped data, or use evaluateHandle() for a live object or unsuitable serialized value.
ReferenceError for a Node variable inside the evaluated function The worker runs the function in a browser worker context; it cannot access the Node.js lexical scope. Pass required values as arguments to evaluate(fn, ...args) and use worker globals in the function.
Code expects window or DOM methods Dedicated workers do not run in the page’s window context. Use APIs available in the worker environment, such as self; perform DOM work in the page context instead.
waitForFunction() times out The predicate stayed false, the wrong worker was selected, or the desired state is not observable in that worker. Check the worker URL and predicate, use an appropriate timeout, and make the condition reflect state available in the worker global.
Worker methods or options differ from an example Puppeteer references can describe different releases, and API signatures evolve. Check the documentation matching the installed package version and pin Puppeteer in the project when reproducible behavior matters.

10. Performance, reliability, and cost considerations

  • Attach only the listeners you need. Lifecycle events are useful when reacting to changes; a snapshot is simpler when you only need current workers.
  • Keep evaluations focused. Return small serializable results and avoid repeatedly transferring large structures from the browser process.
  • Bound waits. Set a timeout or use an abort signal for waits that could remain pending when the worker never reaches the requested state.
  • Handle worker disappearance. A worker can be destroyed while automation is running. Keep the destruction event for visibility and handle evaluation failures around workers whose lifetime is not controlled by your script.
  • Close the browser reliably. Put browser cleanup in a finally block so errors in navigation or evaluation do not leave the launched browser running.
  • Budget for browser automation. This workflow runs a browser and consumes local or hosted compute; actual time and infrastructure cost depend on the page, environment, and workload. The supplied Puppeteer references publish no performance benchmark or cost figure.

11. FAQ

Does page.workers() include workers created after I call it?

No. It returns the workers associated with the page at the time of the call. Use workercreated to observe later creations.

Can I construct a Puppeteer WebWorker myself?

No. The API marks its constructor internal and says third parties should not construct or subclass it. Obtain worker objects through page events or page.workers().

Does evaluateOnNewDocument() run inside a worker?

The documented behavior covers page documents and child frames, not worker contexts. Do not assume it is a worker injection mechanism.

What should I log to identify a worker?

Start with worker.url(), which the official example uses to report the worker’s script URL. Pair it with creation and destruction events when tracking lifecycle.

Or skip the browser setup

If your goal is a clean screenshot of a page rather than worker inspection, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns PNG, JPEG, WebP, or PDF; its API documentation describes the request options.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.