ScreenshotNeo

BlogHow-to

How to Set a Consistent Viewport for Website Screenshots in Puppeteer

Set Puppeteer’s viewport before navigation, choose reliable readiness and capture settings, and keep screenshots comparable across runs.

By the ScreenshotNeo team4 October 20267 min read

To get a consistent screenshot in Puppeteer, set an explicit viewport before navigating, keep the same device scale and emulation settings for every run, wait for the page state you need, and choose viewport-only or full-page capture deliberately.

const puppeteer = require('puppeteer');

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

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

    await page.goto('https://example.com', { waitUntil: 'networkidle2' });
    await page.screenshot({ path: 'page.png' });
  } finally {
    await browser.close();
  }
})();

The 1280 × 800 values are an example desktop profile, not a Puppeteer standard. Pick dimensions for the layout you want to capture and reuse them. Puppeteer’s setViewport documentation recommends configuring the viewport before navigation because some sites do not expect their size to change after loading. See also the Puppeteer screenshot guide, ScreenshotOptions, and Viewport interface.

1. Choose and fix a viewport profile

page.setViewport() accepts a viewport object, or null to reset it. Width and height are CSS pixels. There is no universally correct screenshot size: choose the dimensions that represent the desktop, tablet, or mobile state you intend to inspect.

Capture goal Settings to keep stable Notes
Desktop comparison Width, height, device scale factor Choose one desktop profile that suits the page layout.
Mobile layout Width, height, device scale factor, mobile and touch emulation Configure emulation before navigation. Some changes such as isMobile or hasTouch may reload the page.
Responsive breakpoint checks A named set of profiles, each with fixed values Use multiple deliberate profiles rather than an arbitrary viewport for each run.

For repeatable comparisons, record the profile with the capture: viewport width and height, device scale factor, mobile/touch/orientation fields, browser and Puppeteer version, URL, and readiness condition. A fixed viewport standardizes the configured page dimensions; it does not guarantee identical pixels when browser environments or page content change.

2. Set the viewport before navigation

await page.setViewport({
  width: 1280,
  height: 800,
  deviceScaleFactor: 1,
  isMobile: false,
  hasTouch: false,
  isLandscape: true,
});

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

The viewport API exposes width, height, deviceScaleFactor, isMobile, hasTouch, and isLandscape. Set the fields relevant to your test explicitly. The documented device scale factor default is 1, but making it explicit makes the capture profile easier to review and maintain.

For a device profile, Puppeteer also provides page.emulate(device), which combines user-agent and viewport emulation. Use the same device profile in every run of that test; do not mix device emulation with separately chosen viewport values without documenting which settings take precedence in your setup.

3. Wait for the right page state

waitUntil: 'networkidle2' is a useful navigation option and appears in Puppeteer’s screenshot example, but it is not proof that every dynamic element, animation, font, or lazy-loaded image has settled. For app-rendered content, wait for a meaningful selector or application condition before capture:

await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-screenshot-ready="true"]');
await page.screenshot({ path: 'dashboard.png' });

Use a selector your application adds only when the content to be captured is ready. If the page has continuously active network requests, a network-idle condition may never become appropriate; choose a narrower readiness signal instead.

4. Choose the capture area

By default, page.screenshot() captures the visible viewport. Set fullPage: true when the artifact should include the entire page. For a rectangle, use clip; for one element, use an element handle’s screenshot method.

// Visible viewport
await page.screenshot({ path: 'viewport.png' });

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

// One element
const card = await page.waitForSelector('.product-card');
await card.screenshot({ path: 'card.png' });

The screenshot guide notes that element screenshots try to scroll a hidden element into view by default. A full-page capture changes the output dimensions and may require more memory and time than a viewport-sized image, so use it only when the whole document is needed.

5. Complete runnable example

This CommonJS script sets the viewport before loading the URL, waits for navigation and an application-specific selector, selects viewport or full-page capture from an environment variable, and closes the browser even if navigation or capture fails.

const puppeteer = require('puppeteer');

(async () => {
  const url = process.env.TARGET_URL || 'https://example.com';
  const fullPage = process.env.FULL_PAGE === '1';
  const browser = await puppeteer.launch();

  try {
    const page = await browser.newPage();
    await page.setViewport({
      width: 1280,
      height: 800,
      deviceScaleFactor: 1,
      isMobile: false,
      hasTouch: false,
      isLandscape: true,
    });

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

    // Replace with a selector that signals readiness for your page, if needed.
    // await page.waitForSelector('[data-screenshot-ready="true"]', { timeout: 15_000 });

    await page.screenshot({
      path: fullPage ? 'page-full.png' : 'page-viewport.png',
      fullPage,
      type: 'png',
    });
  } finally {
    await browser.close();
  }
})();

Run it with TARGET_URL=https://example.com node capture.js. Set FULL_PAGE=1 for the whole page. Install Puppeteer in the project using its current official setup instructions, and check the installed version when implementing because APIs and defaults can evolve.

6. Screenshot options worth knowing

Option or method When to use it
fullPage Capture the whole page instead of only the visible viewport; default is false.
clip Capture a chosen rectangle when a fixed region is the target.
ElementHandle.screenshot() Capture one element; hidden elements are scrolled into view by default.
captureBeyondViewport Controls capture beyond the viewport where supported; consult the ScreenshotOptions documentation for the installed version.
type, quality, omitBackground Control output format, lossy image quality, and transparency as supported by the screenshot options and selected format.

Keep image format and any format-specific settings stable as well as viewport settings if you compare output files. The viewport controls layout dimensions; screenshot options control what part of the rendered page is written and in what form.

7. Troubleshooting

Symptom Likely cause Fix
Layout differs between runs Viewport was changed after navigation, or dimensions/emulation differ. Set the full profile before goto() and log it with each capture.
Mobile page looks like desktop Only width and height were set while the site also depends on mobile or touch emulation. Set relevant isMobile, hasTouch, and orientation fields, or use a device profile consistently.
Screenshot is blank or incomplete Capture occurred before the application rendered the target content. Wait for a page-specific selector or state before taking the screenshot.
Navigation times out on an active site The selected network-idle condition may not occur because requests continue. Use a navigation milestone such as domcontentloaded and then wait for the content you need.
Output has unexpected dimensions fullPage, clipping, or device scale factor differs from the intended profile. Check capture options and distinguish CSS-pixel viewport dimensions from output image pixels.
Viewport change reloads the page Some emulation changes, including mobile or touch changes, can trigger a reload. Set these options before navigation and avoid changing them mid-capture.

8. Performance, reliability, and cost

  • Performance: Reuse a browser process for multiple captures when appropriate, while giving each capture a controlled page and profile. Full-page images cover more content and can take more resources than viewport captures. Wait only for the state your task needs.
  • Reliability: Put browser closure in a finally block, set explicit navigation and selector timeouts, and record the viewport and capture options with output. A stable viewport cannot freeze changing page data, fonts, animations, or browser rendering differences.
  • Cost: A local Puppeteer capture has no per-shot ScreenshotNeo API charge, but requires you to run and maintain the browser environment and its infrastructure. For managed capture, account for the service plan and the number of successful captures.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It supports viewport dimensions and other capture settings; the API documentation lists the request options.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com",
        "width": 1280,
        "height": 800,
    },
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
  width: '1280',
  height: '800',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Get 1,000 free screenshots a month with no card.

FAQ

Does a fixed viewport make screenshots pixel-identical?

No. It fixes configured viewport metrics, but page content and rendering environment can still differ.

Should I always use full-page screenshots?

No. Use the default viewport capture for the visible screen and fullPage: true only when you need the document’s full length.

What dimensions should I choose?

Choose dimensions for the layout state you need to inspect, then keep them fixed within that capture profile. Puppeteer does not prescribe one universal size.

Can I reset the viewport?

Yes. The setViewport() method accepts null to reset it; for repeatable captures, an explicit profile is generally easier to reason about.