The shortest working solution is three calls: launch Puppeteer, navigate a page, and call page.screenshot(). Puppeteer launches in headless mode by default, so puppeteer.launch() is already suitable for unattended screenshots. Add fullPage: true for the entire document, an element handle for one component, or a clip rectangle for a precise region.
This guide uses the Puppeteer API documented for version 25.12.0 at the time of writing and explains output formats, timing, viewport control, headless variants, failure recovery, and a hosted alternative when maintaining Chrome is unnecessary.
Install Puppeteer and prepare a script
Use a current Node.js project, then install Puppeteer. The package downloads and manages its bundled browser, which is the browser combination Puppeteer officially guarantees.
mkdir puppeteer-shots
cd puppeteer-shots
npm init -y
npm install puppeteer
Create screenshot.mjs. Using an .mjs file lets you use the documented ES-module import without changing package.json.
Recommended Free Tools
#1 Best Overall
Take a basic headless screenshot
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch(); // headless: true by default
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'screenshot.png' });
} finally {
await browser.close();
}
Run it with node screenshot.mjs. The result is screenshot.png in the process’s current working directory. Puppeteer’s headless guide documents that puppeteer.launch() is equivalent to puppeteer.launch({ headless: true }). The Page.screenshot() API example follows the same launch, navigation, save, and close sequence.
The try/finally matters in a long-running worker: navigation or screenshot errors still close Chromium instead of leaving browser processes behind.
Set a deliberate viewport
Do not rely on an implicit screen size when visual output matters. Set the page viewport before navigation:
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'desktop.png' });
Puppeteer documents an 800×600 headless screen configuration when neither --screen-info nor --window-size is supplied. That is a screen default, not a promise that every screenshot is 800×600; screenshot dimensions also depend on the viewport and capture options. See screen configuration.
Choose what to capture
Viewport versus full page
page.screenshot({ path: 'page.png' }) captures the current viewport because fullPage defaults to false. To include the document below the fold, set it explicitly:
await page.screenshot({
path: 'full-page.png',
fullPage: true
});
Full-page output is useful for an archive or visual regression baseline, while a viewport shot better represents what a user sees without scrolling. The ScreenshotOptions reference defines the available capture options.
Capture one element
Find the element, wait for it to exist, and call its handle’s screenshot() method:
const element = await page.waitForSelector('main');
if (!element) throw new Error('main was not found');
await element.screenshot({ path: 'main.png' });
Puppeteer scrolls the element into view when necessary. Its ElementHandle.screenshot() documentation notes that the call throws if the handle has been detached from the DOM, which can happen when a framework re-renders the component. Re-query the selector immediately before capture if the page is highly dynamic.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Rank #2
Capture a rectangular region
Use clip when a fixed rectangle, rather than a DOM element, is the requirement:
await page.screenshot({
path: 'region.png',
clip: { x: 40, y: 120, width: 800, height: 500 }
});
The coordinates are in CSS pixels relative to the page viewport. A clip is preferable to cropping an already-rendered file when you need the browser to render only the selected area.
Control format, quality, and output
| Option | What it does | Important qualification |
|---|---|---|
path |
Writes the image to disk. | A relative path is resolved against the process’s current working directory. Omit it to receive image bytes instead. |
type: 'png' |
Produces PNG output. | PNG is the documented default. |
type: 'jpeg' |
Produces JPEG output. | Use quality to trade file size against fidelity. |
type: 'webp' |
Produces WebP output where supported by the bundled browser. | Check your downstream image tooling before standardizing on it. |
quality |
Sets lossy image quality from 0 to 100. | It does not apply to PNG. |
omitBackground: true |
Removes the default white page background. | Use it when you need transparency; transparent pixels require a format and consumer that preserve alpha. |
For example, a compact JPEG can be written as:
await page.screenshot({
path: 'card.jpg',
type: 'jpeg',
quality: 82,
clip: { x: 0, y: 0, width: 640, height: 360 }
});
When an API should return an image rather than save one, omit path:
const imageBytes = await page.screenshot({ type: 'png' });
// imageBytes is a Buffer in Node.js; send it in an HTTP response or write it yourself.
Make the page ready before taking the shot
Wait for navigation and content
page.goto() resolves according to its waitUntil setting. Pick a condition that matches the page instead of assuming that the first HTML response contains the final layout.
await page.goto('https://example.com/dashboard', {
waitUntil: 'networkidle2',
timeout: 60_000
});
await page.waitForSelector('[data-testid="chart"]', { timeout: 30_000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });
A selector wait is more meaningful than a fixed sleep when a specific component determines whether the screenshot is useful. If an animation changes the pixels, disable it with CSS or wait for the page’s own completion signal before capturing.
Lazy-loaded images and infinite pages
Full-page capture changes the capture extent, but it does not guarantee that every application has finished loading content triggered by scrolling. For lazy-loaded pages, scroll in controlled increments, wait for images or a known sentinel, then capture. For an infinite feed, define a stopping rule (for example, a maximum scroll height or item count) so a job cannot grow without limit.
Cookies, authentication, and headers
Open a page in the same browser context in which you set the required cookies or authentication state. Keep credentials out of source control and logs. If the site redirects to a login page, verify the final URL and a post-login selector before saving an image; otherwise you may produce a valid screenshot of the wrong page.
Headless mode choices
Regular headless Chrome
Regular headless Chrome is the default and is the safest choice when your automation depends on normal Chrome behavior. Make the setting explicit in configuration when reproducibility matters:
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11const browser = await puppeteer.launch({ headless: true });
chrome-headless-shell
Puppeteer also supports headless: 'shell', which launches the separate chrome-headless-shell binary:
const browser = await puppeteer.launch({ headless: 'shell' });
The official headless-mode guide describes shell mode as the old headless implementation. It does not completely match regular Chrome, although it can be more performant for automation that does not need the complete Chrome feature set. Choose it only after checking compatibility and comparing output in your target environment.
Visible mode for debugging
Use headless: false temporarily when you need to watch navigation, inspect a cookie dialog, or understand why a selector is missing:
const browser = await puppeteer.launch({ headless: false });
Switch back to headless mode in production. Puppeteer’s LaunchOptions type documents headless as boolean | 'shell' with a default of true.
A production-oriented capture function
This reusable function validates the response, sets deterministic dimensions, waits for a meaningful selector, and always closes the browser.
import puppeteer from 'puppeteer';
export async function capture(url, outputPath) {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1366, height: 768, deviceScaleFactor: 1 });
const response = await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60_000
});
if (!response || !response.ok()) {
const status = response ? response.status() : 'no response';
throw new Error(`Navigation failed (${status}) for ${url}`);
}
await page.waitForSelector('body', { timeout: 15_000 });
await page.screenshot({
path: outputPath,
fullPage: true,
type: 'png'
});
} finally {
await browser.close();
}
}
await capture('https://example.com', 'example-full.png');
Record the Puppeteer version, browser channel or binary, headless setting, viewport, and screenshot options with each visual-regression job. Those values are the first things to compare when a developer machine and a CI runner produce different pixels.
Common failures and fixes
“Cannot find module puppeteer” or a missing browser
- Run
npm install puppeteerin the project that executes the script. - Use the bundled browser first. Puppeteer documents that its supported guarantee applies to that browser; using another executable is at your own risk.
- In a restricted CI image, install the operating-system libraries required by the bundled browser or use a container image prepared for Chromium.
The file is saved somewhere unexpected
A relative path uses the process’s current working directory, not necessarily the directory containing the script. Print process.cwd() or pass an absolute, known output directory.
The screenshot is blank or incomplete
- Increase the navigation timeout only after confirming the URL is reachable.
- Use
waitForSelectorfor the component that proves rendering finished. - Check for a redirect, login wall, bot challenge, or JavaScript error by running once with
headless: false. - For lazy content, scroll or trigger the page’s loading mechanism before
fullPagecapture.
An element screenshot throws a detached-node error
The framework replaced the node between lookup and capture. Wait for the update to settle, then call waitForSelector again and capture the new handle. Avoid retaining handles across major navigations or re-renders.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Rank #4
Output differs between machines
Pin the Puppeteer version, use the bundled browser, set the viewport and device scale factor explicitly, and compare fonts, locale, timezone, network responses, and headless mode. Do not assume regular headless Chrome and chrome-headless-shell render identically; Puppeteer explicitly warns that shell behavior is not a complete match.
The process hangs after an error
Put browser shutdown in finally. Also give navigation and selector waits finite timeouts so a failed resource cannot hold a worker forever.
Performance, reliability, and cost decisions
Reuse browsers for batches
Launching Chromium is more expensive than opening a new page. For a batch, launch one browser, create or reuse isolated pages, and close the browser after the batch. Limit concurrency to what the host’s CPU and memory can sustain; too many simultaneous full-page renders can cause timeouts and swapping.
Choose the smallest capture
Viewport or element screenshots use less memory and produce smaller files than an unbounded full-page image. Set an explicit maximum page length for feeds and documentation sites. Use JPEG or WebP only when your quality and alpha requirements allow it; PNG is the predictable default for text and transparency.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteMake retries safe
Retry transient navigation failures with a bounded count and backoff, but do not blindly retry authentication failures or deterministic selector errors. Write to a temporary filename and rename it after a successful capture so consumers never read a partially written file. Keep the URL, status, elapsed time, and final output path in structured logs, while redacting cookies and authorization values.
When a hosted API is simpler
If every job requires a browser image, font setup, cookie handling, timeout policy, and cleanup, the operational cost may exceed the screenshot code itself. A hosted endpoint can move those concerns out of your worker.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It accepts a URL in one request and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.
The direct cURL call is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for authentication and options. The equivalent Python request is:
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)
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 returned ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo includes full-page and selector captures, 12 device presets plus custom viewports, retina scale, dark mode, PDF paper and page-range controls, custom CSS and JavaScript, click actions, waits for selectors, delays or network idle, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed public image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.
Best Value
An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Pricing starts with 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Sign up for the free ScreenshotNeo plan to try it.
Frequently asked questions
Does headless mode require a display server?
No. Puppeteer’s default headless mode runs without opening a visible browser window. Use visible mode only when diagnosing a page interactively.
Can I return screenshot bytes instead of creating a file?
Yes. Omit path from page.screenshot(); Puppeteer returns image data that your Node.js code can send to storage or an HTTP response.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Why might a full-page screenshot be the wrong choice?
A full-page image can be extremely tall, expensive to process, and unlike the viewport a user actually sees. Choose viewport, element, or clipped capture when the consumer needs a bounded region.
Which headless implementation should a new project start with?
Start with regular headless Chrome. Evaluate chrome-headless-shell only when its performance benefit outweighs its incomplete behavioral match for your required features.
Frequently Asked Questions
Can Puppeteer capture a screenshot of a cross-origin iframe?
The iframe must be addressed through its own frame context, and the page must be allowed to load it. If the frame is blocked, redirected, or not yet attached, wait for the frame and its content before capturing the containing page.
How do I make screenshots reproducible in CI?
Pin the Puppeteer version, use its bundled browser, set viewport and device scale explicitly, control fonts and locale, wait on a deterministic selector, and record the headless mode and capture options with each artifact.
Free tools Windows power users keep installed
One-click scans. No signup required.
Is an 800×600 screenshot guaranteed by headless Puppeteer?
No. 800×600 is the documented default headless screen configuration only when no screen or window size is supplied. The screenshot’s actual dimensions depend on the page viewport and capture mode.
Quick Recap
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.




