How to Save Puppeteer Screenshots as JPG
Save Puppeteer screenshots as JPG or JPEG with explicit format, quality control, full-page and element capture, plus fixes for common errors.
Use page.screenshot() with a .jpg or .jpeg path and set type: 'jpeg'. Puppeteer’s default screenshot format is PNG, while JPEG output accepts a quality value from 0 to 100. The following complete example saves the file as screenshot.jpg:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({
path: 'screenshot.jpg',
type: 'jpeg',
quality: 80,
});
} finally {
await browser.close();
}
Install Puppeteer with npm install puppeteer. The quality value is adjustable; Puppeteer documents the 0–100 range but does not define one universally best setting. The API spelling is jpeg, although jpg and jpeg are both common filename extensions. See the ScreenshotOptions reference and ImageFormat type.
1. Set up a minimal Puppeteer project
Create a directory, initialize npm, and install Puppeteer:
mkdir puppeteer-jpg
cd puppeteer-jpg
npm init -y
npm install puppeteer
With an ES module project, add "type": "module" to package.json or save the script with an appropriate module configuration. A CommonJS version uses const puppeteer = require('puppeteer'); instead of the import statement.
2. Choose how Puppeteer writes the JPG
Save directly to a file
await page.screenshot({
path: './output/homepage.jpg',
type: 'jpeg',
quality: 85,
});
Relative paths resolve from the process’s current working directory. Puppeteer infers the output format from the path extension when you omit type, so path: 'screenshot.jpg' is sufficient. Keeping type: 'jpeg' explicit makes the intent clear and avoids surprises when a filename is generated dynamically. If the parent directory does not exist, create it first with fs.mkdir({ recursive: true }).
Keep the image in memory
const bytes = await page.screenshot({
type: 'jpeg',
quality: 80,
});
// bytes is a Uint8Array. For example, write it yourself:
import { writeFile } from 'node:fs/promises';
await writeFile('screenshot.jpg', bytes);
When path is omitted, no disk file is created. page.screenshot() returns a Uint8Array by default. Request base64 text when another API requires it:
const base64 = await page.screenshot({
type: 'jpeg',
quality: 80,
encoding: 'base64',
});
Base64 increases the amount of data your application must carry in memory, so use bytes for file uploads and HTTP responses that accept binary data.
3. Control JPEG quality
await page.screenshot({
path: 'quality-60.jpg',
type: 'jpeg',
quality: 60,
});
quality accepts numbers from 0 through 100. It applies to JPEG and does not apply to PNG. Pick a value based on your visual and storage requirements, then inspect representative pages in your own pipeline; the Puppeteer documentation does not publish a universal quality recommendation or file-size benchmark.
| Option | Meaning |
|---|---|
type: 'jpeg' |
Requests JPEG encoding explicitly. |
quality |
JPEG quality from 0 to 100; ignored for PNG. |
path |
Writes the image to disk; extension can determine format. |
encoding: 'base64' |
Returns base64 instead of a byte array. |
4. Capture full pages, regions, and elements
Full-page JPG
await page.screenshot({
path: 'full-page.jpg',
type: 'jpeg',
quality: 80,
fullPage: true,
});
fullPage: true captures the full document rather than only the current viewport. Pages that lazy-load content may need scrolling or a wait condition before capture so that content has rendered.
Clipped region
await page.screenshot({
path: 'region.jpg',
type: 'jpeg',
quality: 80,
clip: { x: 100, y: 120, width: 800, height: 500 },
});
The clip rectangle is expressed in CSS pixels relative to the page. Ensure the coordinates and dimensions describe an area inside the rendered page.
One element
const card = await page.waitForSelector('.pricing-card');
await card.screenshot({
path: 'pricing-card.jpg',
type: 'jpeg',
quality: 85,
});
ElementHandle.screenshot() scrolls the element into view before capturing it. It throws if the element has detached from the DOM, so reacquire the handle after client-side rerenders. See the ElementHandle screenshot reference.
5. Make the capture deterministic
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.waitForSelector('main');
await page.evaluate(() => document.fonts.ready);
await new Promise(resolve => setTimeout(resolve, 300));
await page.screenshot({ path: 'stable.jpg', type: 'jpeg', quality: 80 });
- Set a fixed viewport so layout breakpoints do not change between runs.
- Use
waitUntil,waitForSelector, a short delay, or a page-specific readiness signal. - Wait for
document.fonts.readywhen font loading changes text wrapping. - Disable animations in a test stylesheet if transitions cause inconsistent frames.
- Use a consistent timezone, locale, and authentication state when the page varies by user or region.
For transparent backgrounds, omitBackground: true removes the default background where the selected output format supports transparency. JPEG does not preserve transparent pixels, so use PNG when transparency is required.
6. Complete reusable script
import puppeteer from 'puppeteer';
import { mkdir } from 'node:fs/promises';
const url = process.argv[2] ?? 'https://example.com';
const output = process.argv[3] ?? 'screenshots/page.jpg';
const browser = await puppeteer.launch();
try {
await mkdir(new URL('.', `file://${process.cwd()}/${output}`).pathname, { recursive: true });
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
await page.screenshot({
path: output,
type: 'jpeg',
quality: 80,
fullPage: true,
});
console.log(`Saved ${output}`);
} finally {
await browser.close();
}
For production code, prefer path.join() for platform-safe paths and validate user-provided URLs before navigating to them.
7. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The file is PNG | type was omitted and the path did not end in .jpg or .jpeg. |
Set type: 'jpeg' and use a JPG extension. |
| Quality has no effect | The output is PNG. | Confirm type: 'jpeg'; quality is not applicable to PNG. |
| No file appears | path was omitted or points to a missing directory. |
Provide a path and create its parent directory. |
TimeoutError during navigation |
The page is slow, blocked, or waiting indefinitely. | Set an appropriate navigation timeout, inspect the URL, and choose a less strict readiness condition when suitable. |
| Blank or partially rendered image | Capture started before content, fonts, or lazy images loaded. | Wait for a selector, fonts, network activity, or an application readiness signal. |
| Element screenshot says the node detached | A framework rerender replaced the element. | Call waitForSelector again immediately before capturing. |
| Clipped area is wrong | Coordinates are CSS pixels and may not match your assumed viewport. | Set the viewport explicitly and calculate the clip from the element’s bounding box. |
| Transparent areas are opaque | JPEG cannot store an alpha channel. | Use PNG with omitBackground: true. |
| Concurrent captures interfere | Some BrowserContext page-creation and close operations wait while a screenshot is running. | Use separate pages or contexts and avoid closing shared resources mid-capture. bringToFront() does not wait for screenshots. |
8. Performance, reliability, and cost
- Reuse a browser process for a batch of URLs, but create isolated pages or contexts when cookies and storage must not leak.
- Capture only the required region or element when a full document is unnecessary.
- Use a lower quality only after checking the visual result and downstream requirements.
- Set explicit navigation and selector timeouts, close pages in
finallyblocks, and record the URL, viewport, wait condition, and output path for diagnosis. - Puppeteer itself has no per-screenshot API charge when you run the browser; budget for CPU, memory, browser downloads, storage, and network traffic.
- Retries should create a fresh page when navigation or rendering state is uncertain. Do not retry indefinitely against a failing origin.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF, so your application does not need to manage Chromium, waits, or file encoding. Read the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan.
Create a free ScreenshotNeo account and try the API without a card.
FAQ
Can I use jpg instead of jpeg for type?
No. Use Puppeteer’s documented type: 'jpeg' value. Use either .jpg or .jpeg for the filename.
What happens if I omit quality?
Puppeteer uses its underlying JPEG encoder defaults. Set an explicit value when you need repeatable encoding settings.
Does fullPage capture content below the fold?
It captures the full document, but lazy-loaded resources may still require scrolling or an explicit wait before the screenshot.
Can I upload the returned screenshot without saving it?
Yes. Omit path and pass the returned Uint8Array to your storage or HTTP client.


