October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
Node.js

How to Take a Full-Page Screenshot of a Single-Page App With Puppeteer

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

Use Puppeteer’s page.screenshot({ fullPage: true }) after your SPA has reached a known rendered state. A reliable capture fixes the viewport, waits for navigation and an app-owned readiness signal, loads content that appears only after scrolling, then saves the image and closes Chromium even when a job fails.

Complete Puppeteer example

Install Puppeteer in a Node.js project, then run this script. Replace the URL and readiness selector with values from your application.

npm install puppeteer
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/app', {
    waitUntil: 'networkidle2',
    timeout: 60000
  });
  await page.waitForSelector('[data-app-ready="true"]', {
    visible: true,
    timeout: 30000
  });
  await page.screenshot({
    path: 'spa-full-page.png',
    fullPage: true
  });
} finally {
  await browser.close();
}

fullPage: true tells Puppeteer to capture the complete document rather than only the current viewport. The viewport is set explicitly because responsive breakpoints can change the page’s layout, number of columns and even which components render. A fixed deviceScaleFactor makes output dimensions and text rasterization more repeatable.

The selector in the example is deliberately application-specific. Add an attribute such as data-app-ready="true" when your app has fetched its data and rendered the state you want captured. If your app cannot expose a selector, use a JavaScript predicate instead.

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.

Why SPAs often produce incomplete screenshots

Navigation completion is not UI completion

Single-page apps commonly load a shell first and fetch route data afterward. waitUntil: 'networkidle2' waits for a period with no more than two active connections, but analytics, WebSockets, polling and other background traffic can keep a page busy. Conversely, a brief quiet period can occur before the meaningful component renders. Treat network idle as a useful lower bound, not proof that the screen is ready.

Lazy content is not in the document yet

Images, cards and sections may be inserted only when they approach the viewport. A full-page capture cannot include elements that the app has not created. Trigger those sections deliberately, wait for their content, and then capture.

Animations can change pixels during capture

Transitions, carousels and blinking carets make visual comparisons unreliable. In test builds, disable motion with a stylesheet or an app setting; otherwise wait until the relevant animation has ended. The exact CSS or flag is application-specific.

Choose the right readiness wait

Use a stable selector for an explicit app state

A marker rendered only after data and critical components are ready is usually the most maintainable approach.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.waitForSelector('#dashboard[data-state="loaded"]', {
  visible: true,
  timeout: 30000
});

Use waitForFunction for a state flag

If the application sets a global flag, wait for that predicate rather than guessing a delay.

await page.waitForFunction(
  () => window.__APP_READY__ === true,
  { timeout: 30000 }
);

Use a delay only for a known, bounded transition

page.waitForTimeout() can cover a short animation, but it does not verify that data arrived. Prefer a selector or predicate whenever possible, and keep any delay small and documented.

When network-idle is appropriate

networkidle0 requires zero active connections and can be useful for a static route with no polling. networkidle2 is less strict and often better for ordinary pages. For an SPA, combine one of them with an app-owned readiness check.

Loading lazy sections before capture

Scroll in controlled increments so intersection observers request deferred content. Wait between increments and, when possible, wait for a marker indicating that the newly exposed section is populated.

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.
async function loadLazyContent(page) {
  await page.evaluate(async () => {
    await new Promise(resolve => {
      let y = 0;
      const step = Math.max(window.innerHeight * 0.8, 400);
      const timer = setInterval(() => {
        window.scrollBy(0, step);
        y += step;
        if (y >= document.body.scrollHeight) {
          clearInterval(timer);
          resolve();
        }
      }, 150);
    });
  });
  await page.waitForFunction(
    () => [...document.images].every(img => img.complete),
    { timeout: 30000 }
  );
  await page.evaluate(() => window.scrollTo(0, 0));
}

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

The image-complete check confirms that browser image requests finished; it does not prove that every framework component has completed. Add a selector or predicate for sections whose data arrives through JavaScript. Some virtualized lists intentionally remove off-screen rows, in which case a single full-document screenshot cannot contain every row; render a non-virtualized print route or capture segments instead.

Viewport, scale and deterministic output

  • Viewport width: choose the width your users or visual tests target. A different width can activate a mobile layout.
  • Viewport height: it affects what loads initially and which intersection observers fire.
  • Device scale factor: use 1 for predictable CSS-pixel output or a higher value when you need retina-like detail.
  • Fonts: wait for web fonts before capture when text metrics matter: await page.evaluate(() => document.fonts.ready).
  • Color scheme and locale: configure them explicitly if the app changes for dark mode, timezone or language.
await page.emulateMediaFeatures([
  { name: 'prefers-color-scheme', value: 'light' }
]);
await page.evaluate(() => document.fonts.ready);

Use a consistent Chromium version in CI and local development when pixel-level diffs are important. Save screenshots with a unique name per job so parallel runs do not overwrite one another.

Full page, viewport, element or PDF?

Goal Puppeteer method Result
Visible screen only page.screenshot() Current viewport
Entire document page.screenshot({ fullPage: true }) Raster image of the document
One rendered component elementHandle.screenshot() Raster image of that element
Printable pages page.pdf() PDF using print CSS by default

Choose page.pdf() only when a PDF is the required output. Print CSS, page breaks and margins make it a different rendering path from a PNG, JPEG or WebP screenshot.

Production reliability and failure handling

Use layered timeouts

Set a navigation timeout long enough for your environment, selector timeouts that reflect backend latency, and an overall job deadline in your worker. A single unbounded wait can exhaust a queue when a site is down.

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

Always close the browser

Put capture code in a try/finally block. Closing the browser releases Chromium processes and temporary resources after both successful and failed jobs.

Record what happened

Log the URL, viewport, browser version, elapsed time and the readiness step that failed. Keep the HTML or a diagnostic screenshot when permitted by your data policy. These details distinguish an app regression from a network failure.

Control external dependencies

Third-party ads, trackers and chat scripts can delay readiness or alter layout. In a controlled test environment, mock APIs or block nonessential requests. Do not hide required application requests merely to make a screenshot pass.

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

Troubleshooting common failures

The screenshot stops at the first viewport

Check that the options object contains fullPage: true and that the document, rather than a fixed-height scrolling container, owns the content. If the app scrolls inside a div, capture that element or temporarily expand it for a print route.

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

Cards or images below the fold are missing

The app is probably lazy-loading them. Run the controlled scroll routine, wait for section-specific markers and verify that virtualization is not discarding off-screen nodes.

waitForSelector times out

Confirm the selector in the browser’s final DOM, including route changes and shadow DOM boundaries. Increase the timeout only after checking that the marker is actually emitted on error states. For shadow roots, expose a test hook or query inside the component from page context.

networkidle2 never resolves

Long polling, WebSockets or analytics may keep connections open. Navigate with waitUntil: 'domcontentloaded' and then wait for your app’s selector or predicate. Keep a separate overall timeout.

The page is blank or shows a bot challenge

Verify the URL, authentication and required headers, then inspect the response and console logs. A challenge may require an approved test environment; do not attempt to bypass access controls.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

Text shifts between runs

Wait for document.fonts.ready, use a fixed viewport and browser version, freeze time or data where your test permits, and disable animations. Differences in remote content can still produce legitimate pixel changes.

Chromium fails to launch in CI

Install the browser revision that your Puppeteer version expects, ensure the runner has required system libraries, and avoid sharing one browser process across unrelated jobs unless you manage isolation and cleanup.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API when you do not want to maintain Chromium. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing result.

One GET request returns PNG, JPEG, WebP or PDF. The API supports full-page and element captures, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, selector or network-idle waits, lazy-image loading, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage data and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

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

See the ScreenshotNeo documentation for the current parameters. This call saves a WebP response:

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

The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.

Equivalent calls in Python and Node.js

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/app"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/app' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Frequently Asked Questions

Can Puppeteer capture a page inside an iframe?

Yes, obtain the target frame with page.frames() and query its DOM. Cross-origin restrictions still apply to what browser JavaScript can inspect.

What does fullPage do with a very tall document?

Puppeteer asks Chromium to render the document’s full layout. Extremely tall pages can consume substantial memory; split captures or provide a print-specific route when necessary.

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

Should I reuse one browser for many screenshots?

A shared browser can reduce launch overhead, but isolate pages, clear state and enforce per-job timeouts. Launch separate browser processes when stronger fault isolation is more important than throughput.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.