How to Run JavaScript Before Printing a PDF with Headless Chromium
Wait for page JavaScript to finish, choose the right readiness signal, then print a reliable PDF with Puppeteer and Headless Chromium.

To run JavaScript before printing a PDF with headless Chromium, navigate with Puppeteer, wait for the page’s required asynchronous work to complete, and then call page.pdf(). Use a meaningful readiness condition when one exists. Network idle is a useful navigation milestone, but it does not prove that application data or print layout is ready.
Puppeteer generates PDFs with the print CSS media type by default. You can wait for a page-defined flag with page.waitForFunction(), await a known asynchronous page function with page.evaluate(), or combine those checks with network-idle waiting. The complete workflow is:
- Launch Chromium and create a page.
- Navigate to the report or document.
- Wait for the application’s data and layout to finish.
- Choose print or screen media.
- Set paper, margins, colors, fonts and page-range options.
- Generate the PDF and close the browser in a
finallyblock.
1. A complete Puppeteer example
This example waits for navigation to reach networkidle2, then waits for a page-defined window.reportReady flag. The flag is application-specific: your page must set it after its data, charts and print layout are ready. Puppeteer does not create this property for you.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com/report', {
waitUntil: 'networkidle2',
});
await page.waitForFunction(
() => window.reportReady === true,
{ timeout: 30_000 }
);
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
waitForFonts: true,
});
} finally {
await browser.close();
}
The navigation and PDF sequence follows the Puppeteer PDF guide. The waitForFunction predicate resolves when it becomes truthy, as described in the Page.waitForFunction API.
Make the page expose readiness
In an application you control, set the flag only after the work that affects the PDF has completed:
async function renderReport() {
const response = await fetch('/api/report');
const report = await response.json();
renderCharts(report);
await document.fonts.ready;
window.reportReady = true;
}
renderReport().catch((error) => {
console.error(error);
window.reportReady = false;
});
If the page has no readiness flag, wait for a stable selector, a known DOM property, or a callable function that represents completion. Do not copy reportReady as though it were a Chromium signal.
2. Await a specific asynchronous JavaScript operation
When the page exposes a function that prepares the document, call it with page.evaluate(). Puppeteer waits for a returned Promise to settle:

await page.evaluate(async () => {
await window.prepareReportForPrint();
});
await page.pdf({
path: 'report.pdf',
printBackground: true,
});
prepareReportForPrint must exist on the target page. It is not a Puppeteer method. This pattern is useful when the site can load data, expand sections, finish chart rendering, or apply a print-only layout through one controlled operation. See the Puppeteer evaluate API for page-context execution behavior.
Pass data into the page function
const reportId = '2026-09';
await page.evaluate(async (id) => {
await window.prepareReportForPrint(id);
}, reportId);
Arguments passed to evaluate are serialized into the page context. Keep the function self-contained and avoid relying on Node.js variables that were not passed as arguments.
3. Choosing the right wait condition
| Condition | What it proves | Use it when | Limitation |
|---|---|---|---|
waitUntil: 'networkidle2' |
Navigation reached a period with very little network activity | The page’s requests settle after loading | Open connections, polling or lazy work can make it early or late |
page.waitForNetworkIdle() |
Network activity stayed idle for the configured period | You need an idle-network checkpoint after another action | Idle traffic does not mean application rendering is complete |
page.waitForFunction() |
A page-defined predicate became truthy | The application exposes a completion flag or state | The predicate must be correct and observable |
page.evaluate() |
A known page Promise resolved | The page owns a callable preparation function | The function must exist and handle failures |
page.waitForSelector() |
A selector appeared or reached a state | A stable element marks completion | Presence alone may not mean its content is final |
The official PDF example uses networkidle2 before printing. For dynamic applications, pair that milestone with an application-specific condition. Avoid an arbitrary fixed sleep as the only readiness check when a meaningful condition can be tested.
4. PDF media, colors and layout
page.pdf() uses print CSS by default. If the page’s screen layout is the desired output, switch media before generating the file:
await page.emulateMediaType('screen');
await page.pdf({
path: 'screen-layout.pdf',
printBackground: true,
});
Use print media when the stylesheet contains an intentional print layout. Use screen media when the PDF should resemble the interactive page. The emulateMediaType API controls this choice.
Important PDF options
format: paper preset such asA4orLetter.widthandheight: explicit dimensions when a preset is not suitable.landscape: rotate the page for wide tables or dashboards.margin: set top, right, bottom and left margins with CSS units.scale: adjust rendered size when content is too large or too small.pageRanges: export selected pages, such as1-3.printBackground: include background graphics; it is false by default.waitForFonts: wait for fonts before output; the documented default is true.preferCSSPageSize: let a CSS@pagesize take priority over explicit dimensions.
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
landscape: false,
printBackground: true,
waitForFonts: true,
preferCSSPageSize: true,
margin: {
top: '18mm',
right: '14mm',
bottom: '18mm',
left: '14mm',
},
pageRanges: '1-4',
});
These options are documented in Puppeteer’s PDFOptions reference. PDF colors are adjusted for print by default. When exact colors matter, use CSS such as:
@media print {
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
5. A robust production workflow
Set explicit timeouts
A readiness wait without a timeout can leave a worker stuck when a page fails. Configure navigation, selector and function waits for the expected workload:
page.setDefaultNavigationTimeout(45_000);
page.setDefaultTimeout(30_000);
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45_000 });
await page.waitForFunction(() => window.reportReady === true, {
timeout: 30_000,
polling: 'mutation',
});
Choose polling that matches the signal. Mutation polling works when the page changes DOM state; interval polling can be useful for a value updated by a timer.
Fail clearly and clean up
try {
await page.goto(url, { waitUntil: 'networkidle2' });
await page.waitForSelector('[data-report-complete]', {
visible: true,
});
await page.pdf({ path: output, printBackground: true });
} catch (error) {
console.error(`PDF generation failed for ${url}:`, error);
throw error;
} finally {
await browser.close();
}
Close the browser even on navigation, JavaScript or PDF errors. In a service, isolate jobs in separate pages, cap concurrent Chromium processes, and record the URL, wait condition and failure stage so retries are diagnosable.
Fonts, images and lazy content
Fonts can change line wrapping and page count. Keep waitForFonts: true unless you have a measured reason to disable it. Lazy images may not load until they enter the viewport; trigger the page’s own loading mechanism or scroll before waiting for the final readiness signal. A successful network-idle event can still produce a PDF with missing content if the application schedules work afterward.
6. Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
waitForFunction times out |
The flag is never set, has a different name, or a page exception stopped rendering | Inspect browser console errors, verify the predicate in DevTools, and set the flag only after successful rendering |
| PDF contains a loading spinner | Navigation completed before application data finished | Wait for a data-loaded selector, page flag or preparation Promise |
| Charts or backgrounds are missing | Print backgrounds are disabled or print CSS hides the content | Set printBackground: true and inspect print media rules |
| Screen styling is missing | page.pdf() selected print media |
Call page.emulateMediaType('screen') before printing |
| Wrong page size | Explicit dimensions conflict with CSS @page |
Use preferCSSPageSize: true or remove the conflicting setting |
| Fonts wrap differently | Web fonts were not ready or were blocked | Keep waitForFonts: true, verify font responses and wait for document.fonts.ready |
| Navigation never reaches network idle | Analytics, WebSockets or polling keep connections open | Use a shorter navigation milestone and an application readiness predicate |
| Blank or partial PDF | Authentication, bot protection, a failed request or an early print call | Check response status, cookies and console errors; wait for the actual completion condition |
| Process memory grows | Browsers or pages are not closed after jobs | Use finally, recycle workers and cap concurrency |
7. Performance, reliability and cost considerations
PDF generation has at least three phases: page navigation, application rendering and PDF layout. Measure them separately in logs. Reducing an arbitrary delay can make jobs faster, but replacing it with a reliable readiness check is safer than simply lowering the sleep value.
Reuse a launched browser when appropriate, create a fresh page per job, and avoid unbounded parallelism. Heavy pages, large images, custom fonts and long reports consume more CPU and memory. Set job-level deadlines and retry only transient failures such as a failed navigation. Retrying a deterministic JavaScript exception will not fix the page.
For predictable output, pin the Puppeteer version used by the project and review the documentation for that version. Puppeteer APIs and defaults can change. Store a small set of representative PDFs in regression checks if pagination, colors or charts are business-critical.
8. Or skip the browser setup
ScreenshotNeo provides a website capture API and MCP server when you want a PDF without maintaining Chromium orchestration. Its PDF options include paper size, margins, landscape mode and page ranges. You can also run custom JavaScript, wait for a selector, delay or network idle, and supply headers, cookies, a user agent, authorization, timezone and geolocation.

One call returns the result:
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 PDF parameters and response details. The same endpoint can return PNG, JPEG, WebP or PDF depending on the requested options.
ScreenshotNeo removes cookie and consent banners, newsletter popups and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result. Its 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 each month with no card; paid plans start at $5 for 3,000 shots.
9. Calling the same capture from Python and Node.js
Python
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(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));
Use the API when you need repeatable capture infrastructure, bulk requests, signed links, caching with a chosen TTL, asynchronous jobs with signed webhooks or usage reporting. Every feature is available on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots per month with no card.
10. Short FAQ
Does page.pdf() execute JavaScript?
The page’s JavaScript runs during navigation and interaction. page.pdf() prints the current rendered state; it does not provide a general “run all JavaScript now” step. Wait for the work your application needs.
Is networkidle2 always enough?
No. It describes network activity, not whether your report’s data, charts or layout are complete. Add an application-specific condition when the page has one.
How do I print screen CSS?
Call await page.emulateMediaType('screen') before page.pdf().
Why is my PDF missing background colors?
Set printBackground: true and check whether print CSS changes or hides the element.
Can I print only selected pages?
Yes. Pass a pageRanges value such as '1-3' in the PDF options.


