How to Improve Headless Chrome PDF Quality for Large Documents
Improve Puppeteer PDF quality for long documents with print CSS, sizing, fonts, readiness checks, streaming, diagnostics, and production guidance.

Direct answer: Treat Headless Chrome PDF generation as print rendering. Define print CSS and page geometry deliberately, choose whether CSS @page or Puppeteer paper options control size, enable backgrounds when the design needs them, wait for fonts and application content, and validate representative long documents. For large files, measure render time, browser memory, output size, and failure rates rather than relying on an assumed page limit: Puppeteer’s documentation does not publish a universal page-count, DOM-size, output-size, or memory ceiling.
This guide uses Puppeteer’s current PDF APIs and links to the official references: PDFOptions, Page.pdf(), Page.createPDFStream(), the PDF generation guide, headless modes guide, and the browser support table.
1. Make print rendering intentional
page.pdf() renders with the print media type. Screen CSS is not a reliable preview of the PDF. Add a print stylesheet that controls visibility, colors, page breaks, and typography.

<link rel="stylesheet" href="/app.css">
<style media="print">
@page {
size: A4;
margin: 18mm 16mm 20mm;
}
html, body {
margin: 0;
color: #111;
background: #fff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.screen-only,
.cookie-banner,
.chat-widget {
display: none !important;
}
h1, h2, h3 {
break-after: avoid;
}
table, figure, pre, blockquote {
break-inside: avoid;
}
.page-break {
break-before: page;
}
</style>
Use break-before, break-after, and break-inside for modern pagination. Keep headings with the following content and prevent tables, figures, and code blocks from splitting where possible. Test the result with the same Chrome version used in production.
2. Choose one source of truth for page size
Puppeteer can take size from CSS @page or from format, width, and height. The preferCSSPageSize option decides which wins. Its default is false; Chrome scales content to fit the selected paper size. Set it to true when exact CSS page geometry matters.

| Requirement | Recommended configuration |
|---|---|
| CSS controls page dimensions | @page { size: ...; margin: ... } plus preferCSSPageSize: true |
| One standard paper size | Use format: 'A4', 'Letter', or another supported format |
| Exact custom dimensions | Use width and height with explicit units |
| Consistent margins | Set margins in one place and verify the output visually |
The scale option accepts values from 0.1 to 2 and defaults to 1. Scaling changes the rendered content size; it does not repair incorrect CSS layout. Start at 1 and change it only for a documented output requirement.
3. Preserve colors, backgrounds, and typography
printBackground defaults to false. Enable it for colored headers, cards, charts, gradients, and background images. Chrome modifies colors for printing by default; -webkit-print-color-adjust: exact asks it to preserve the CSS colors.
const pdf = await page.pdf({
path: 'report.pdf',
printBackground: true,
preferCSSPageSize: true,
scale: 1,
displayHeaderFooter: false
});
Wait for fonts explicitly when font metrics affect wrapping. Puppeteer waits for document.fonts.ready by default during PDF generation, but application data, images, charts, and client-side rendering need their own readiness checks.
await page.evaluate(async () => {
await document.fonts.ready;
const images = Array.from(document.images);
await Promise.all(images.map(img => {
if (img.complete) return img.decode?.().catch(() => {});
return new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
});
await page.waitForSelector('[data-report-ready="true"]', { timeout: 30000 });
Set the readiness marker in your application only after data fetching and chart rendering finish. A network-idle event alone cannot prove that a single-page application has completed its work.
4. A complete Puppeteer implementation
This Node.js example navigates to a report, waits for application readiness, applies print settings, and writes a PDF.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com/report', {
waitUntil: 'networkidle2',
timeout: 120000
});
await page.emulateMediaType('print');
await page.waitForSelector('[data-report-ready="true"]', { timeout: 30000 });
await page.evaluate(async () => {
await document.fonts.ready;
await Promise.all(Array.from(document.images).map(img =>
img.complete ? (img.decode?.().catch(() => {})) : new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
})
));
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: false,
margin: { top: '18mm', right: '16mm', bottom: '20mm', left: '16mm' },
scale: 1,
displayHeaderFooter: false,
timeout: 120000
});
} finally {
await browser.close();
}
If CSS @page defines the exact size and margins, remove the Puppeteer margin and format settings and use preferCSSPageSize: true. Do not maintain two conflicting geometry systems.
5. Headers, footers, and page numbering
Set displayHeaderFooter: true and provide HTML templates when you need repeating metadata. The templates support classes such as pageNumber, totalPages, date, title, and url.
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:8px;width:100%;text-align:right;padding:0 16mm">Quarterly report</div>',
footerTemplate: '<div style="font-size:8px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
margin: { top: '24mm', bottom: '22mm', left: '16mm', right: '16mm' }
});
Reserve enough top and bottom margin for these templates. Header and footer CSS is separate from the document’s normal stylesheet, so inline styles are the safest choice.
6. Large-document strategy
Measure representative documents
Build a corpus that includes the longest reports, largest tables, image-heavy pages, custom fonts, charts, and worst-case data. Record navigation time, PDF generation time, output byte size, browser process memory, and failure reason. The official Puppeteer references do not define a universal maximum, so production limits must come from your measurements and infrastructure.
Keep the page stable
- Use deterministic data and freeze animations before capture.
- Give images explicit dimensions to reduce layout shifts.
- Load only resources needed by the report.
- Wait for a page-specific readiness signal.
- Use one browser instance per job or a controlled pool, with cleanup in a
finallyblock.
Use a stream when your consumer needs one
page.pdf() returns a Uint8Array. page.createPDFStream() returns a ReadableStream<Uint8Array>. Streaming changes how your application receives bytes; Puppeteer does not promise that it lowers Chrome’s layout or rendering memory use.
const stream = await page.createPDFStream({
format: 'A4',
printBackground: true,
preferCSSPageSize: true
});
const writer = (await import('node:fs')).createWriteStream('report.pdf');
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
writer.write(Buffer.from(value));
}
} finally {
writer.end();
}
Partition only as an architecture decision
If measurements show that one document exceeds your practical memory or timeout budget, split it at deliberate section boundaries and validate page numbering, links, bookmarks, and cross-section requirements. Puppeteer’s documentation provides no universal split threshold.
7. Browser mode and version control
Puppeteer documents standard new headless Chrome and chrome-headless-shell. The shell may be more performant for some automation workloads but has reduced compatibility; this is not a promise of better PDF fidelity. Compare both modes on your actual reports before changing production.
Pin the Puppeteer package and record the browser version in build logs. Puppeteer’s browser mapping is version-sensitive; check the supported browsers table for the package you install. From Puppeteer v20, Chrome for Testing is downloaded by default.
8. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Colors or backgrounds are missing | printBackground is false or print color adjustment changes them |
Set printBackground: true and use -webkit-print-color-adjust: exact. |
| Everything is unexpectedly shrunk | CSS page size conflicts with format, or default fitting is active |
Choose one size authority and review preferCSSPageSize. |
| Fonts wrap differently | Fonts were not loaded, or the production browser differs | Wait for document.fonts.ready, verify font URLs, and pin browser versions. |
| Charts or data are blank | PDF started before client rendering completed | Wait for a page-specific readiness selector or application event. |
| Images are missing | Lazy loading, decode delay, blocked requests, or broken URLs | Scroll or trigger lazy loading, wait for image completion and decode, and inspect failed requests. |
| Page breaks split important content | No print break rules or oversized unbreakable elements | Use break-inside: avoid, explicit section breaks, and realistic width constraints. |
| Header overlaps content | Header template exists but top margin is too small | Increase the PDF top margin and keep template CSS inline. |
| Navigation or PDF times out | Slow resources, blocked requests, or an application deadlock | Log request failures, use a realistic timeout, and wait for an explicit readiness condition. |
| High memory or intermittent crashes | Very large DOM, images, concurrent jobs, or leaked browser pages | Measure representative files, limit concurrency, close pages and browsers, optimize assets, and partition only after validating document semantics. |
9. Reliability, performance, and cost considerations
- Reliability: Log URL, Puppeteer version, browser version, options, timings, output size, and the first failed request. Retry navigation only when the failure is transient; avoid blindly rerunning deterministic application errors.
- Performance: Reuse a controlled browser pool when startup dominates, but cap concurrent pages according to measured CPU and memory. Disable unnecessary third-party resources and animations.
- Quality: Compare PDFs from every browser update using representative fixtures. Check page count, text extraction, fonts, colors, images, page breaks, and metadata.
- Cost: The main variables are browser CPU time, memory, storage, bandwidth, and concurrency. Track them per document and set limits based on measurements rather than a guessed page maximum.
10. Or skip the browser setup
If you need a screenshot or PDF endpoint instead of maintaining Chrome infrastructure, ScreenshotNeo accepts one GET request and returns PNG, JPEG, WebP, or PDF. Its capture options include full-page output with lazy images loaded, custom CSS and JavaScript, waiting for a selector, delay or network idle, PDF paper size, margins, landscape mode and page ranges, custom headers and cookies, blocking rules, caching, async jobs, bulk capture, and signed webhooks.
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}`);
See the ScreenshotNeo API documentation for PDF parameters and response details. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can call take_screenshot, get_page_info, and capture_pdf.
The Free plan includes 1,000 screenshots per 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.
11. FAQ
Does networkidle2 guarantee a complete PDF?
No. It is a navigation wait condition. Application data, fonts, images, and charts can still be unfinished, so add page-specific readiness checks.
Should I always set preferCSSPageSize to true?
Use it when CSS @page is your intended authority. Use Puppeteer’s paper options when your service owns page geometry. The important part is choosing deliberately.
Does createPDFStream() solve browser out-of-memory errors?
It changes the byte-return interface to a readable stream. Puppeteer does not document it as a lower-memory rendering mode.
Can I rely on a fixed maximum page count?
No universal limit is documented. Measure your own documents, browser version, concurrency, and infrastructure.
Is chrome-headless-shell higher quality?
Not by documentation. It may be faster for some automation tasks but has reduced compatibility. Validate output on your target workload.


