What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Use page.evaluate() in Playwright or Page.evaluate() in Puppeteer. The callback runs inside the loaded page, where window and document exist; the value you return crosses back to your automation script. The two environments do not share lexical variables, so pass every outside value as an explicit argument. Await the call when the callback is asynchronous.

The execution model: two JavaScript worlds

A headless test has an automation process and a browser page. Your Node.js (or other binding) code controls navigation and calls an evaluation method. The callback itself executes in the page’s JavaScript context, with browser globals such as window, document, location, and the page’s DOM available. Playwright documents this boundary in its JavaScript evaluation guide; Puppeteer’s equivalent is Page.evaluate().

Do not treat the callback as a closure over your automation script. A variable declared outside is not visible inside unless you pass it:

const selector = 'h1';
const text = await page.evaluate((css) => {
  return document.querySelector(css)?.textContent ?? null;
}, selector);

Values crossing the boundary should be serializable data: strings, numbers, booleans, arrays, or plain objects. A DOM node is a live browser object, not an ordinary JSON result. Use an evaluation handle when you need to keep and manipulate that object in the page.

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

Playwright: a complete evaluation example

The following script navigates, reads the title and first heading, passes a selector into the page, performs asynchronous page-side work, and closes the browser. It assumes Playwright is already installed in your project.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

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

  const pageTitle = await page.evaluate(() => document.title);
  const heading = await page.evaluate((selector) => {
    return document.querySelector(selector)?.textContent?.trim() ?? null;
  }, 'h1');

  const status = await page.evaluate(async () => {
    const response = await fetch(location.href);
    return response.status;
  });

  console.log({ pageTitle, heading, status });
  await browser.close();
})();

The first callback reads a browser global. The second receives 'h1' as its argument and returns a string or null. The third is marked async; Playwright waits for its returned promise and gives your script the resolved HTTP status. A failed navigation, blocked request, or page-side exception still rejects the surrounding operation, so catch errors where your application can report or recover from them.

Passing multiple arguments

Pass one serializable value or an object containing several values. This keeps the boundary explicit and avoids accidentally depending on automation-side state.

const result = await page.evaluate(({ selector, suffix }) => {
  const node = document.querySelector(selector);
  return node ? `${node.textContent.trim()}${suffix}` : null;
}, { selector: '.price', suffix: ' USD' });

Evaluating in a frame

If the content you need lives in an iframe, obtain that frame from Playwright and call its evaluation method. The same context rules apply: code runs in that frame, and values must be passed and returned explicitly. A selector in the top-level page cannot directly query a different frame’s document.

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

Puppeteer: the equivalent API

Puppeteer exposes the same concept through Page.evaluate(). Its current API reference is versioned (the reference retrieved for this article reports 25.12.0), so check the current method documentation when upgrading.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

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

  const title = await page.evaluate(() => document.title);
  const heading = await page.evaluate((selector) => {
    return document.querySelector(selector)?.textContent?.trim() ?? null;
  }, 'h1');

  const status = await page.evaluate(async () => {
    const response = await fetch(location.href);
    return response.status;
  });

  console.log({ title, heading, status });
  await browser.close();
})();

Puppeteer also waits for a promise returned by the callback. The API spelling and argument shape are nearly identical to Playwright’s, so the decision should follow your project’s runtime, browser setup, and existing automation code rather than an assumed universal performance winner. Chrome for Developers describes Puppeteer as a high-level browser automation API; its supported browser details remain release-sensitive.

Returning data versus keeping a page object

Use ordinary evaluation when you need a value that can leave the page:

  • text, attributes, URLs, dimensions, and computed numbers;
  • arrays or plain objects assembled from several elements;
  • a status code or other result from asynchronous page-side work.

For a live DOM element or another JavaScript object that must remain in the browser, use a handle. Puppeteer’s JavaScript-execution guide explains that returning an element through normal serialization does not create a usable live element reference; use evaluateHandle() or an ElementHandle. Playwright provides evaluateHandle() for the same purpose. Dispose of handles when your framework requires explicit cleanup.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// Playwright: keep the object in the browser instead of serializing it
const elementHandle = await page.evaluateHandle(() => document.querySelector('h1'));
// Use the handle with the framework's handle APIs, then dispose it when finished.
await elementHandle.dispose();

Do not return a function, a DOM node, or a complex browser object when your caller expects JSON-like data. Extract the properties you need inside the callback and return a small plain object instead.

Choose the browser mode deliberately

“Headless” is not one identical implementation. Playwright documents both a Chromium headless shell and a newer Chromium mode selected through the chromium channel. The newer mode uses the real Chrome browser, while the shell can differ in rendering and behavior. See the Playwright browser guide.

If your production issue depends on Chrome-specific behavior, run the channel that matches production and record that choice in your test configuration. If you are validating only data extraction, either mode may be sufficient, but you should still keep the mode stable between runs. Puppeteer likewise depends on the browser binary it launches; the exact binary and version are part of the result, not incidental details.

Playwright and Puppeteer compared for evaluation

Concern Playwright Puppeteer
Evaluation method page.evaluate(callback, arg) page.evaluate(callback, arg)
Where callback runs In the page’s JavaScript context In the page’s JavaScript context
Async callback Returned promises are awaited Returned promises are awaited
Live object reference evaluateHandle() evaluateHandle() or ElementHandle
Browser-mode consideration Headless shell versus real Chrome channel is documented Behavior follows the launched browser binary and version
Best fit Projects already using Playwright’s browser and context controls Projects already using Puppeteer’s Chrome-oriented automation API

The documented evaluation behavior is substantially the same. Compare the surrounding project APIs, supported language/runtime setup, and browser binary you must operate; the available material does not establish a universal speed or reliability ranking.

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

Reliable evaluation patterns

Wait for the state you actually inspect

Evaluation runs against the page state that exists at the moment of the call. Navigate first, then wait for the DOM, a specific selector, or the application state your check requires. A callback that queries an element before client-side rendering finishes can correctly return null even though the element appears a moment later.

Keep callbacks small

Do DOM traversal and page-side computation in one callback when that reduces round trips, but return only the data the automation process needs. Smaller results serialize faster and are easier to log and validate.

Handle failures at the automation boundary

Wrap navigation and evaluation in your test runner’s error handling. Distinguish a missing element (a valid null result in the example) from a thrown page exception, a rejected promise, or a browser crash. Include the URL, selector, browser mode, and a short operation name in your error record.

Clean up pages, browsers, and handles

Close pages and the browser in a finally block in long-running jobs. Dispose of evaluation handles after use. This prevents detached browser processes and accumulated remote objects from degrading later evaluations.

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

Troubleshooting common errors

“ReferenceError: selector is not defined”

Cause: the callback attempted to read a variable from the automation script’s lexical scope.
Fix: pass it as an argument, for example page.evaluate((css) => document.querySelector(css), selector).

The result is null

Cause: the selector did not match in that document or the page had not rendered the element yet.
Fix: verify the selector in the correct frame and wait for the page state your application promises before evaluating.

A returned element is an empty object or unusable value

Cause: DOM objects are not ordinary serializable data.
Fix: return text or attributes, or retain the object with evaluateHandle()/ElementHandle.

An async callback returns too soon

Cause: the promise was not returned or awaited inside the callback.
Fix: mark the callback async and return the awaited operation, as in return (await fetch(location.href)).status.

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

The same code behaves differently in CI and locally

Cause: different browser binaries, headless modes, versions, viewport settings, or page timing.
Fix: pin and report the browser mode/version, use the mode matching your target environment, and wait for a deterministic readiness condition.

Evaluation fails after navigation

Cause: navigation failed, the page closed, a frame was replaced, or page-side code threw an exception.
Fix: capture the navigation error, confirm the page is still open, reacquire a replaced frame, and log the original page exception instead of masking it with a generic timeout.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When you only need a clean visual capture

If your goal is a screenshot or PDF rather than a value returned from page JavaScript, ScreenshotNeo removes the browser setup. One GET request captures a URL as PNG, JPEG, WebP, or PDF. It accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response reports its page verdict and billing status in X-Page-Verdict and X-Billed headers.

Or skip the browser setup

Use the documented endpoint and options at ScreenshotNeo’s API documentation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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}`);

ScreenshotNeo also provides an MCP server for AI agents such as Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. Every feature is on every plan: 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

Frequently asked questions

Can the same evaluation callback be reused on a frame?

Yes. Call the frame’s evaluation method rather than the top-level page’s method, and pass arguments exactly as you would for the page. The callback then sees that frame’s document.

Does Puppeteer run only in headless mode?

No. Headless is a launch choice. Puppeteer can drive a visible browser as well; the important point for reproducibility is to record the binary, version, and mode used.

Where should I check for release-specific behavior?

Use the Playwright evaluation and browser guides and Puppeteer’s versioned API and JavaScript-execution guides linked above before upgrading, because evaluation and headless-browser details can change with releases.

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

Frequently Asked Questions

Can the same evaluation callback be reused on a frame?

Yes. Call the frame’s evaluation method rather than the top-level page’s method, and pass arguments exactly as you would for the page. The callback then sees that frame’s document.

Does Puppeteer run only in headless mode?

No. Headless is a launch choice. Puppeteer can drive a visible browser as well; record the binary, version, and mode for reproducible results.

Where should I check for release-specific behavior?

Check the linked Playwright and Puppeteer documentation before upgrades; evaluation and headless-browser details are release-sensitive.

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.

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