How to Control PDF Output Quality and Size with Puppeteer
Control Puppeteer PDF fidelity and file size with documented options, repeatable measurements, print CSS, and practical troubleshooting.
Direct answer: Puppeteer controls PDF appearance and layout through Page.pdf(); it does not document a PDF compression or image-downsampling option. Start with print CSS, paper geometry, margins, backgrounds, font readiness, and page selection. Then measure the resulting byte count and page count while changing one rendering input at a time. Treat scale as a rendering control, not as a guaranteed compression setting.
Puppeteer generates PDFs with the print CSS media type by default. Use page.emulateMediaType('screen') only when the screen stylesheet is the intended design. The official Page.pdf() documentation and PDF generation guide describe the rendering behavior and defaults; neither documents a compression ratio, automatic image downsampling, or a guarantee that any option reduces file size.
1. A complete Puppeteer PDF example
Install Puppeteer, create a page with print-specific CSS, wait for the document to settle, and write the PDF to disk:
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
// Add executablePath here when your deployment supplies Chromium separately.
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto('https://example.com/report', {
waitUntil: 'networkidle2',
timeout: 60_000
});
// Use this only when the screen design should be printed.
// await page.emulateMediaType('screen');
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'report.pdf',
format: 'A4',
margin: {
top: '18mm',
right: '16mm',
bottom: '18mm',
left: '16mm'
},
printBackground: true,
preferCSSPageSize: true,
scale: 1,
waitForFonts: true,
tagged: true
});
} finally {
await browser.close();
}
})();
For an HTML string instead of a remote page, use page.setContent() and wait for fonts before calling page.pdf():
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: 'invoice.pdf', format: 'Letter', printBackground: true });
2. Decide whether print or screen CSS is correct
page.pdf() uses print media by default. A print stylesheet may hide navigation, change colors, simplify grids, or add page-break rules:
@page {
size: A4;
margin: 18mm 16mm;
}
@media print {
.navigation, .chat-widget, .cookie-banner { display: none !important; }
.avoid-break { break-inside: avoid; }
h1, h2 { break-after: avoid; }
}
/* Request closer color matching when backgrounds are intentional. */
html {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
Call await page.emulateMediaType('screen') before page.pdf() when the PDF should use screen styles. Puppeteer notes that print rendering can modify colors; print-color-adjust requests the specified colors but affects appearance, not documented file compression.
3. Control paper geometry and pagination
| Option | Documented behavior | Practical use |
|---|---|---|
format |
Defaults to Letter and takes precedence over width and height. |
Choose a standard paper size such as A4 or Letter. |
width, height |
Set custom paper dimensions. | Use for receipts, labels, or other nonstandard output. |
preferCSSPageSize |
Defaults to false. When true, CSS @page size takes priority; otherwise content is scaled to fit the API paper size. |
Make the stylesheet authoritative when it owns page geometry. |
margin |
Defaults to no margins. | Set all four margins explicitly for predictable layout. |
pageRanges |
Empty string prints every page. | Export only required pages; this selects pages rather than compressing them. |
Keep CSS and Puppeteer geometry consistent. If @page { size: A4; } and format: 'Letter' express different intentions, choose one authority and set preferCSSPageSize: true when CSS should win. Confirm page breaks and image dimensions before changing scale.
4. Understand scale, backgrounds, and fonts
scale: defaults to1and accepts values from0.1through2. It changes rendering scale and can alter pagination and readability. The API does not describe it as compression.printBackground: defaults tofalse. Enable it when background colors, gradients, or images are part of the intended design. Omitting them may reduce visual content, but it is a fidelity decision.waitForFonts: defaults totrue. Keep font waiting enabled when typography matters. A deliberate alternative should be based on your own rendering requirements.tagged: the current API documents this as experimental with a default oftrue. Validate accessibility output before changing it, and do not change it solely to seek smaller files without measurements.
Images and fonts often dominate PDF size, but Puppeteer’s documented PDF options do not promise image recompression or font subsetting. Optimize those inputs in the page or in a separate, measured post-processing step that your application explicitly supports.
5. Measure quality and size with a repeatable workflow
- Fix the input URL or HTML, assets, fonts, browser version, viewport, and network conditions.
- Choose the target paper size and write print-specific CSS.
- Generate a baseline PDF with explicit margins and the intended media type.
- Record file bytes and page count.
- Change exactly one option or input, then generate another PDF.
- Inspect text sharpness, images, colors, font fallback, page breaks, and accessibility requirements.
- Keep the smallest output that still meets the visual and functional requirements.
const fs = require('node:fs/promises');
const pdf = await fs.readFile('report.pdf');
console.log({ bytes: pdf.byteLength });
// Use a PDF parser in your own toolchain to record page count.
// Keep Puppeteer and Chromium versions in the test record.
Do not report an expected percentage reduction unless you measured that exact page, asset set, Puppeteer version, and Chromium version. The documented API provides no universal size benchmark.
6. Configuration patterns
Custom paper size
await page.pdf({
path: 'receipt.pdf',
width: '80mm',
height: '220mm',
margin: { top: '4mm', right: '4mm', bottom: '4mm', left: '4mm' },
printBackground: true
});
CSS-controlled pages
await page.pdf({
path: 'catalog.pdf',
preferCSSPageSize: true,
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }
});
Export selected pages
await page.pdf({
path: 'summary.pdf',
format: 'A4',
pageRanges: '1-2,5',
printBackground: true
});
7. Troubleshooting Puppeteer PDF output
| Symptom | Likely cause | Fix |
|---|---|---|
| Screen colors or layout are missing | PDF generation uses print media and backgrounds are disabled by default. | Call emulateMediaType('screen') when appropriate and set printBackground: true when backgrounds belong in the design. |
| Content is unexpectedly scaled | format conflicts with width/height, or CSS page size is not authoritative. |
Use one geometry strategy; set preferCSSPageSize: true when @page should win. |
| Fonts are wrong or text reflows | Web fonts were not ready when the PDF was created. | Keep waitForFonts: true and explicitly await document.fonts.ready; verify font requests are successful. |
| Images are blank | Lazy loading or remote assets had not completed. | Wait for the relevant selector or network activity, scroll to trigger lazy images, and verify asset responses. |
| Backgrounds are absent | printBackground defaults to false. |
Enable it and check print-color-adjust rules. |
| PDF is larger after changing scale | Scale changes rendering and pagination; it is not documented as compression. | Restore the readable scale and optimize page assets separately, measuring each change. |
| Only part of the document appears | pageRanges restricts output or CSS page breaks hide content. |
Use an empty page range for all pages and inspect print CSS. |
| Navigation times out | Slow or blocked resources prevent the selected waitUntil condition. |
Set a suitable timeout, diagnose failed requests, and choose a readiness condition that matches the page. |
8. Performance, reliability, and cost considerations
- Reuse a browser process: launch Chromium once and create isolated pages for multiple jobs. Close pages after each job to limit memory growth.
- Wait for the right readiness signal:
networkidle2is useful for many pages, but applications with analytics or long polling may never become truly idle. A specific selector or application-ready flag can be more reliable. - Keep inputs deterministic: pin the Puppeteer and Chromium versions used for visual comparisons, and keep fonts and remote assets stable.
- Control failures: set navigation and PDF timeouts, capture console and request-failure logs, and retry only failures that are safe to repeat.
- Measure bytes and pages: rendering options can change pagination, which can change both output size and processing time.
- Separate rendering from compression: if your delivery requirements require a smaller file, evaluate a separately supported PDF optimization tool after rendering. Record quality checks and output bytes for that pipeline.
9. Or skip the browser setup
For a hosted screenshot or PDF workflow, ScreenshotNeo provides a GET endpoint and an MCP server. Its capture pipeline accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers. The MCP tools take_screenshot, get_page_info, and capture_pdf work with Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options, including PDF paper size, margins, landscape mode, and page ranges.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', body);
ScreenshotNeo includes full-page capture with lazy images loaded, custom CSS and JavaScript, waiting for selectors or network idle, request blocking, custom headers and cookies, device presets, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture, and a usage API. It also accepts the parameter names used by other screenshot APIs, which can simplify migration.
There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.
10. FAQ
Does Puppeteer compress PDFs?
The documented Page.pdf() API does not expose a PDF compression or image-downsampling setting. Measure your output and use a separate optimization stage if your requirements call for one.
Should I lower scale to reduce file size?
Not automatically. Scale changes rendering and may affect readability and pagination. Test it as a visual parameter and keep it only when the measured result meets your requirements.
Why does my PDF look different from the browser tab?
PDF generation uses print media by default, and print backgrounds are disabled by default. Use screen media and backgrounds only when those are the intended output.
Which option controls the number of pages?
Geometry, margins, scale, CSS breaks, and content determine pagination. pageRanges only selects which generated pages are included.
Can I make Puppeteer wait for web fonts?
Yes. waitForFonts defaults to true, and you can also await document.fonts.ready before calling page.pdf().


