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.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use a real browser, not an HTTP request, when you need a screenshot of what a user sees after JavaScript, lazy loading, consent dialogs, and other client-side behavior have run. Playwright and Puppeteer both provide the required sequence: open a page, wait for the content your capture needs, then call the screenshot API. The critical part is the readiness condition. A load event or an apparently quiet network is not proof that an application has finished rendering.

The basic Playwright solution

Install Playwright and its browser binaries in your Node.js project:

npm install playwright
npx playwright install chromium

This complete script opens a URL, waits for a page-specific element, and saves a full-page PNG:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const { chromium } = require('playwright');

const url = process.argv[2] || 'https://example.com';

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });

    await page.goto(url, {
      waitUntil: 'domcontentloaded',
      timeout: 30_000
    });

    // Replace this with a selector that means “ready” on your target site.
    await page.locator('main').waitFor({ state: 'visible', timeout: 15_000 });

    await page.screenshot({
      path: 'capture.png',
      fullPage: true
    });
  } finally {
    await browser.close();
  }
})();

Run it with node capture.js https://your-site.example. If the target has no main element, use a stable selector such as [data-testid="results"], a heading, or a component that appears only after the required data has arrived. A generic selector that exists before rendering is not a useful readiness signal.

Choose the right readiness condition

Navigation milestones

domcontentloaded means the initial HTML has been parsed. load waits for the page’s load event and its dependent resources. Neither guarantees that a client-rendered application has fetched and displayed its data. You can request load when that is the site’s meaningful boundary:

await page.goto(url, { waitUntil: 'load', timeout: 30_000 });

Playwright also exposes networkidle, but its API documentation discourages using network-idle as a general testing readiness strategy. Analytics, polling, advertisements, and open connections can keep a page busy, while a page can become network-idle before a later interaction renders the content you need. Prefer a web assertion or selector that represents the actual result.

Wait for a selector

await page.locator('#invoice').waitFor({ state: 'visible', timeout: 20_000 });

Waiting for a selector is usually the clearest approach for dashboards, search results, and server responses that insert a known component.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Wait for a state or application condition

await page.waitForFunction(() => {
  return document.querySelector('[data-status]')?.getAttribute('data-status') === 'ready';
}, null, { timeout: 20_000 });

Use a condition tied to the application, such as a status attribute, a result count, or the disappearance of a loading indicator. Keep the callback deterministic and return a boolean.

Wait for a bounded delay only when necessary

await page.waitForTimeout(2_000);

A fixed delay can cover an animation or a third-party widget, but it is slower on fast runs and still unreliable on slow runs. Combine it with a selector or state check whenever possible.

Capture only what the user needs

Viewport versus full page

// What is currently visible in the viewport
await page.screenshot({ path: 'viewport.png' });

// The entire scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });

Full-page capture can produce a very tall image and may expose content that a user would need to scroll to. Use viewport capture for a visual check of the current screen; use fullPage for documentation or archival output.

Capture one element

const card = page.locator('.profile-card');
await card.waitFor({ state: 'visible' });
await card.screenshot({ path: 'profile-card.png' });

Element screenshots are useful for a chart, receipt, or component. Make the element visible first and ensure its fonts and images have loaded.

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

Control format and quality

await page.screenshot({
  path: 'capture.webp',
  type: 'webp',
  quality: 82,
  fullPage: true,
  animations: 'disabled'
});

PNG is lossless and suitable for text-heavy images. JPEG and WebP are smaller; quality applies to lossy formats. Disabling animations prevents a capture from landing on an arbitrary animation frame.

Return bytes instead of writing a file

const bytes = await page.screenshot({ type: 'png' });
// bytes is a Buffer; send it in an HTTP response or write it elsewhere.

Handle pages that require interaction

A loaded page may still be waiting for a click, login, or consent choice. Perform those actions before the final readiness check:

await page.goto('https://example.com/account', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByLabel('Email').fill(process.env.EMAIL);
await page.getByLabel('Password').fill(process.env.PASSWORD);
await page.getByRole('button', { name: 'Continue' }).click();
await page.locator('[data-testid="account-home"]').waitFor({ state: 'visible' });
await page.screenshot({ path: 'account.png', fullPage: true });

Do not put credentials directly in source code. Use environment variables or a secret manager, and avoid storing screenshots that contain passwords, tokens, personal data, or private account pages.

For a persistent login, create a browser context with a saved storage state, protect that file as a credential, and never commit it to source control:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const page = await context.newPage();

Equivalent Puppeteer workflow

Puppeteer provides the same browser-based model. Install it with npm install puppeteer:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900 });
    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
      timeout: 30_000
    });
    await page.waitForSelector('main', { visible: true, timeout: 15_000 });
    await page.screenshot({ path: 'capture.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Puppeteer’s guide uses networkidle2 as an example navigation wait. Treat it as a navigation heuristic, not a universal definition of application readiness; add a selector or state check for the content you actually need.

To capture a single element, Puppeteer documents ElementHandle.screenshot():

const element = await page.waitForSelector('.invoice', { visible: true });
await element.screenshot({ path: 'invoice.png' });

Playwright supports multiple browser engines, while Puppeteer is centered on its supported Chromium-oriented workflow. Select the library whose browser coverage and APIs match your deployment; neither library is universally faster or more reliable for every site.

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

Screenshot versus rendered page data

If “capture” means extracting the HTML or text a user sees rather than producing an image, use Playwright’s page-context evaluation:

const rendered = await page.evaluate(() => ({
  title: document.title,
  text: document.querySelector('main')?.innerText ?? '',
  html: document.querySelector('main')?.outerHTML ?? ''
}));
console.log(rendered);

The callback runs in the page context. Return strings, arrays, or plain objects that can be serialized to Node.js. A non-serializable return value resolves to undefined. Use page.screenshot() when the required result is pixels, and evaluate() when it is rendered DOM-derived data.

Make captures repeatable

  • Set the viewport and device scale. Responsive breakpoints change the output. Choose explicit width, height, and, when needed, deviceScaleFactor.
  • Freeze unstable visuals. Disable animations, hide rotating banners, or wait for a stable state.
  • Wait for fonts and images. A selector can appear before its web font or lazy image. For a page-specific implementation, check the required image elements or use a short, bounded post-render wait.
  • Control locale and timezone. Dates, number formats, and conditional content can vary. Configure the browser context to match the user you are representing.
  • Use a known user agent only when required. Changing it can alter the page and may trigger bot defenses; do not claim to be a browser you cannot support.
  • Close every browser. Put cleanup in finally so timeouts do not leave Chromium processes consuming memory.

For high-volume jobs, reuse a browser process while creating isolated contexts per capture, cap concurrent pages, and set navigation and selector timeouts. A single page can consume substantial memory, especially for full-page images, so measure your own target pages before choosing concurrency.

Troubleshooting common failures

Symptom Likely cause Fix
Browser executable is missing Playwright was installed without its browser binaries, or the runtime image omits them. Run npx playwright install chromium during the build, or use a deployment image that includes the required browser.
Timeout in goto Slow server, redirect loop, blocked request, or an unreachable URL. Check the URL from the same host, inspect redirects, raise the timeout only when justified, and log the final URL.
Selector timeout The selector is wrong, content is behind a click/login, or the application failed. Inspect the page in headed mode, verify the selector, perform required interaction, and capture console or page-error logs.
Blank or partial screenshot Capture occurred before client rendering, images, fonts, or lazy content completed. Wait for the content-specific state and the assets that matter; do not rely only on a fixed delay.
Consent dialog covers the page The site requires a user decision before showing content. Handle the dialog according to the site’s terms, click the appropriate control, then wait for the covered content.
Different output in production Viewport, locale, timezone, cookies, fonts, or user agent differ from development. Set those values explicitly and record them with the capture metadata.
Bot check or CAPTCHA The destination is challenging automation. Do not attempt to bypass a CAPTCHA. Use an authorized integration, a permitted authenticated flow, or ask the site owner for access.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need a screenshot service rather than maintaining Chromium, ScreenshotNeo is the first alternative to try: it removes cookie banners, newsletter popups, and chat widgets before capture, bills only clean shots, and has a $5 paid plan for 3,000 shots.

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

One GET request returns an image or PDF. The API accepts the URL and access key as query parameters; the response identifies the page result and billing status in X-Page-Verdict and X-Billed headers.

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(`ScreenshotNeo HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', data);

See the ScreenshotNeo API documentation for all parameters and response details. The service supports full-page and element capture, dark mode, device presets or custom viewports, retina scale, PDF options, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture, usage reporting, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work, which can simplify migration.

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)

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response reports what happened. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Cost, reliability, and security decisions

  • Self-hosted browser: no per-shot service charge, but you maintain browser binaries, fonts, memory, isolation, queues, and site-specific waits.
  • Hosted API: less infrastructure and easier scaling, but you must protect API keys, account for network transfer, and review the provider’s handling of URLs and page data.
  • Reliability: log URL, final URL, timing, viewport, readiness selector, browser errors, HTTP status, and screenshot format so failures are diagnosable.
  • Security: treat target URLs as untrusted input. Restrict outbound network access where appropriate, avoid exposing internal services to a screenshot worker, and do not render secrets into artifacts.

FAQ

Can Node.js screenshot a page without a browser?

Not if you need the rendered result of JavaScript, CSS, fonts, and layout. An HTTP client can download source HTML, but a browser engine is required to reproduce the user’s rendered view.

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

Should I use Playwright or Puppeteer?

Both implement navigation and screenshots. Decide from the browser engines, APIs, and deployment support your project requires; the available documentation does not establish a universal speed or reliability winner.

Can I capture a page after a user scrolls?

Yes. Use page.locator(...).scrollIntoViewIfNeeded() or page.evaluate(() => window.scrollTo(...)), wait for any lazy content triggered by that scroll, and then capture.

Frequently Asked Questions

Can Node.js screenshot a page without a browser?

Not when the required result is JavaScript-rendered pixels. Use a browser engine such as Playwright or Puppeteer.

Should I use Playwright or Puppeteer?

Both support the workflow. Choose according to the browser coverage, APIs, and deployment environment your application needs.

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

Can I capture content that loads after scrolling?

Yes. Scroll to the relevant region, wait for its lazy content or readiness state, and then call the screenshot method.

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.