ScreenshotNeo

BlogHow-to

Using Website Screenshot JavaScript Locally

Capture rendered websites with local JavaScript using Puppeteer or Playwright, with reliable waits, full-page and element shots, and troubleshooting.

By the ScreenshotNeo team1 October 20268 min read

Use a local Node.js script with Puppeteer or Playwright. Launch a real browser, set the viewport, navigate to the URL, wait for the content your image needs, capture the viewport, full page or an element, then close the browser. The same workflow works for client-rendered sites because the browser executes their JavaScript before the screenshot.

The smallest Puppeteer flow is launch, open a page, navigate, capture, and close. Puppeteer’s official example follows this sequence in its Page API documentation.

1. Choose Puppeteer or Playwright

Both libraries document JavaScript screenshot workflows. Puppeteer focuses on browser automation for Chrome and Firefox through Chrome DevTools Protocol and WebDriver BiDi; Playwright exposes Chromium, Firefox and WebKit browser support. Choose the library already used by your project, then check the installed version’s API for exact options. The available documentation does not establish that one is universally better.

Use case Good starting point
Existing Chrome automation project Puppeteer
Testing several browser engines Playwright
One small script with minimal setup Either; use the official getting-started guide

2. Create a local Node.js project

mkdir local-site-shot
cd local-site-shot
npm init -y
npm install puppeteer

Puppeteer downloads a compatible browser during installation in its standard setup. If your environment manages browsers separately, follow the installation instructions for the version you use.

3. Capture a viewport with Puppeteer

Save this as screenshot.mjs and run node screenshot.mjs. The try/finally ensures the browser is closed when navigation or capture fails.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'screenshot.png' });
} finally {
  await browser.close();
}

Puppeteer’s screenshots guide demonstrates page.goto() followed by a screenshot. Treat networkidle2 as an example readiness condition, not a guarantee that every site has finished rendering; applications with polling, advertisements or delayed data often need a more specific wait. See the Puppeteer screenshots guide.

4. Capture with Playwright

Install Playwright and its browser binaries:

npm install -D playwright
npx playwright install chromium

Save this as playwright-shot.mjs:

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 1
  });
  const page = await context.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  await page.screenshot({ path: 'screenshot.png' });
} finally {
  await browser.close();
}

Playwright’s Page API shows the same browser-context and page model. Set the viewport before navigation because many sites do not expect a phone or desktop layout to change after the page has loaded.

5. Pick the right capture mode

Viewport screenshot

A normal page screenshot captures only what is visible in the current viewport:

await page.screenshot({ path: 'viewport.png', type: 'png' });

Full-page screenshot

Playwright can capture the page’s scrollable area:

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

Full-page mode is useful for articles and landing pages, but long pages can create very large images. Playwright documents that fullPage: true cannot be combined with an element target.

Element screenshot

Use a stable CSS selector when you need one card, chart or component. Puppeteer scrolls an element into view when necessary:

const card = await page.$('[data-testid="pricing-card"]');
if (!card) throw new Error('pricing card was not found');
await card.screenshot({ path: 'pricing-card.png' });

With Playwright:

await page.locator('[data-testid="pricing-card"]').screenshot({
  path: 'pricing-card.png'
});

Image format and scale

PNG preserves lossless detail. JPEG is smaller for photographic pages and accepts a quality value. WebP can reduce size when your downstream tools support it. Playwright documents PNG, JPEG and WebP output; verify options against the installed library version.

await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 82 });
await page.screenshot({ path: 'page.webp', type: 'webp', quality: 82 });

deviceScaleFactor controls device pixels per CSS pixel. A value of 2 produces a sharper retina-style image and roughly increases pixel count fourfold, which affects memory and file size.

6. Wait for the content you actually need

Navigation finishing does not mean that a single-page application, image, chart or font is ready. Use a condition tied to the visual result:

await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="dashboard-ready"]', { timeout: 30000 });
await page.waitForTimeout(250); // optional: allow a final animation frame
await page.screenshot({ path: 'dashboard.png' });

For a specific image, wait until it has loaded:

await page.waitForFunction(() => {
  const image = document.querySelector('main img');
  return image && image.complete && image.naturalWidth > 0;
}, { timeout: 30000 });

Use network-idle waits only when they match the site. A chat connection, analytics request or polling loop can prevent network idle indefinitely; a selector or application-ready flag is usually more deterministic.

7. Control layout before capture

Set the viewport before navigation:

await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });

Emulate a mobile viewport deliberately rather than resizing after the page has loaded:

await context = browser.newContext({
  viewport: { width: 390, height: 844 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true
});

For deterministic captures, disable animations and transitions with injected CSS:

await page.addStyleTag({ content: `
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    caret-color: transparent !important;
  }
` });

Hide an element that obscures the page only when doing so reflects your intended output:

await page.addStyleTag({ content: `
  .cookie-banner, .chat-widget { display: none !important; }
` });

8. Handle lazy loading and dynamic pages

Lazy images may load only after they enter the viewport. For a full-page capture, scroll through the document before taking the shot:

await page.evaluate(async () => {
  await new Promise((resolve) => {
    let y = 0;
    const step = 600;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 100);
  });
});
await page.waitForTimeout(500);
await page.screenshot({ path: 'lazy-loaded.png', fullPage: true });

This is a practical technique, not a universal guarantee. Some applications require clicking a “load more” control or waiting for a framework-specific state. Prefer an explicit selector or API response when one exists.

9. Add authentication, headers and cookies

For a page that needs basic authentication, pass credentials to navigation:

await page.authenticate({ username: process.env.SITE_USER, password: process.env.SITE_PASSWORD });
await page.goto('https://staging.example.com');

Set headers before navigation:

await page.setExtraHTTPHeaders({
  'X-Preview-Token': process.env.PREVIEW_TOKEN,
  'Accept-Language': 'en-US'
});

For a session cookie, create it before opening the target URL:

await page.setCookie({
  name: 'session',
  value: process.env.SESSION_VALUE,
  domain: 'example.com',
  path: '/',
  secure: true,
  httpOnly: true
});
await page.goto('https://example.com/account');

Keep secrets in environment variables and never commit them with the screenshot script.

10. Make scripts reliable in CI

  • Always close the browser in finally, including on timeout.
  • Use explicit navigation, selector and screenshot timeouts.
  • Write output to a known artifact directory and create it before capture.
  • Use a fixed viewport, timezone and locale when visual diffs matter.
  • Log the URL, wait condition and failure stage, but avoid logging credentials or cookies.
  • Retry transient navigation failures with a small bounded retry count; do not hide persistent selector or authentication errors.
const timeout = 30000;
page.setDefaultTimeout(timeout);
page.setDefaultNavigationTimeout(timeout);

try {
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout });
  await page.waitForSelector('#app-ready', { timeout });
  await page.screenshot({ path: outputPath, fullPage: true });
} catch (error) {
  console.error(`Screenshot failed for ${targetUrl}:`, error.message);
  throw error;
} finally {
  await browser.close();
}

11. Troubleshooting common failures

Symptom Likely cause Fix
Blank or mostly empty image Capture ran before the app rendered Wait for a page-specific ready selector or data state.
Timeout waiting for network idle Polling, analytics or open connections never become idle Use domcontentloaded plus a selector, or wait for a known response.
Missing images Lazy loading or failed image requests Scroll to trigger lazy loading, wait for complete, and inspect failed requests.
Wrong mobile layout Viewport changed after navigation Set viewport and device scale before opening the page.
Element not found Selector is unstable or content is inside an iframe Use a data attribute, wait for the element, or select the correct frame.
Cookie banner covers content Consent UI is part of the rendered page Accept it through the UI, set an appropriate consent cookie, or hide it only for a clearly defined capture.
Browser executable missing Browser binaries were not installed in the environment Run the library’s browser install command and cache the binaries in CI.
Navigation blocked Authentication, bot protection, certificate or robots policy Check the response and browser console, provide valid credentials, and follow the site’s access rules.
Huge memory use Very large full-page image or high device scale Capture sections or an element, reduce scale, and avoid unbounded page height.

12. Performance, reliability and cost

Launching a browser is the expensive part of a one-off script. For a batch, reuse one browser process and create a fresh context or page per job. Limit concurrency so several full-page, high-scale captures do not exhaust memory. Reuse a fixed browser installation in CI instead of downloading it for every run.

Capture only the pixels you need. A viewport or element image is faster and smaller than a tall full-page image. Lowering deviceScaleFactor, using JPEG/WebP, and blocking irrelevant resources can reduce work, but blocking CSS, fonts or images can change the visual result.

Local automation has no service charge, but it consumes CPU, memory, storage and engineering time. You also own browser updates, sandbox configuration, retries, proxy access, consent handling and failure reporting. Measure your own pages; the cited documentation provides API behavior, not universal speed or reliability benchmarks.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One request returns a PNG, JPEG, WebP or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits cost nothing, and the response identifies the verdict and billing status with X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options, including full-page and CSS-selector captures, dark mode, device presets, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, async webhooks, bulk capture and usage reporting.

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)
open("shot.webp", "wb").write(r.content)

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}`);

An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does a screenshot include the browser toolbar?

No. Puppeteer and Playwright page screenshots capture the rendered web page, not the operating-system window or browser chrome.

Should I use a fixed delay?

Use a selector, response or application-ready condition when possible. A short delay can allow an animation frame to settle, but it is less reliable than waiting for the state you need.

Can I capture an iframe?

Yes, but locate the iframe’s frame and query inside that frame. A selector on the parent page cannot directly select the iframe document.

Can I run this without a display server?

Yes. Chromium-based automation normally runs headless in CI; follow the installed library’s sandbox and dependency guidance for your operating system.

What should I store with a screenshot artifact?

Record the target URL, viewport, browser/library version, capture mode and timestamp. Those values make visual differences reproducible.