Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsSome links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
If your HTML is rendered by JavaScript, generate the PDF in a real browser. Navigate with Puppeteer or Playwright, run page-context code with evaluate(), wait for a readiness signal owned by your application, then call page.pdf(). This preserves charts, asynchronously loaded data, web fonts and print CSS more reliably than converting the initial HTML string.
The browser workflow that produces complete PDFs
A dependable pipeline has five explicit phases:
- Load the document. Use
page.goto()for a route orpage.setContent()for an HTML string. - Run setup JavaScript. Use
page.evaluate()for code that should execute after the page is available. Use an init script such as Puppeteer’sevaluateOnNewDocument()when code must run before page scripts. - Expose readiness. Have the application set a flag, dispatch an event, or render a known selector after data, charts and fonts are finished.
- Apply PDF media settings. PDF generation uses print CSS by default. Select screen media only when the screen layout is intentional for the document.
- Generate and inspect. Call
page.pdf(), then check representative pages for clipping, missing backgrounds, incorrect page breaks and unloaded assets.
Puppeteer’s guide describes Page.pdf() as the API for printing PDFs. Its reference says PDF generation uses the print CSS media type. Playwright also returns a PDF buffer and uses print media by default.
Complete Puppeteer example with custom JavaScript
Install and run
Install Puppeteer in a Node.js project, save the following as render-pdf.mjs, and run it with node render-pdf.mjs. Replace the URL with the page that owns your report.
npm install puppeteer
import puppeteer from 'puppeteer';
const url = 'https://example.com/report';
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 });
await page.evaluate(async () => {
// This code runs in the browser, so window and document are available.
document.documentElement.classList.add('pdf-export');
// Ask the application to finish work that is specific to this export.
if (typeof window.renderChartsForPdf === 'function') {
await window.renderChartsForPdf();
}
// Wait for fonts and every image that is already in the document.
if (document.fonts) {
await document.fonts.ready;
}
await Promise.all(Array.from(document.images).map((image) => {
if (image.complete) return image.decode ? image.decode().catch(() => {}) : Promise.resolve();
return new Promise((resolve) => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
});
}));
// Set this only after your data, charts and layout are ready.
window.__PDF_READY__ = true;
});
await page.waitForFunction(() => window.__PDF_READY__ === true, {
timeout: 30000
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate: '<span class="pageNumber"></span> / <span class="totalPages"></span>',
margin: { top: '18mm', right: '14mm', bottom: '18mm', left: '14mm' }
});
await browser.close();
The function passed to evaluate() is serialized and executed in the page, not in Node.js. It can use browser globals such as window and document, but it cannot directly access Node modules, local variables or server credentials. Pass only serializable arguments when you need to provide data.
#1 Best Overall
Rendering an HTML string instead of a URL
For generated markup, replace navigation with:
await page.setContent(html, { waitUntil: 'networkidle0' });
Use absolute URLs for stylesheets, images and fonts, or serve the assets from a reachable local origin. Relative paths without a meaningful base URL are a common reason a PDF has unstyled text or broken images.
Playwright implementation in Python
Playwright exposes the same essential sequence: navigate, evaluate in the page, wait for an application condition and call page.pdf(). This asynchronous example writes the returned bytes to disk.
pip install playwright
playwright install chromium
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": 1440, "height": 1000})
await page.goto('https://example.com/report', wait_until='networkidle', timeout=60000)
await page.evaluate('''async () => {
document.documentElement.classList.add('pdf-export');
if (typeof window.renderChartsForPdf === 'function') {
await window.renderChartsForPdf();
}
if (document.fonts) await document.fonts.ready;
window.__PDF_READY__ = true;
}''')
await page.wait_for_function('window.__PDF_READY__ === true', timeout=30000)
pdf_bytes = await page.pdf(
path='report.pdf',
format='A4',
print_background=True,
prefer_css_page_size=True,
margin={'top': '18mm', 'right': '14mm', 'bottom': '18mm', 'left': '14mm'}
)
await browser.close()
asyncio.run(main())
Playwright’s PDF method returns a buffer as well as supporting a path. Set emulate_media(media='screen') before generation only when you deliberately want screen rules; otherwise keep the default print media.
Recommended Free Tools
How to execute custom JavaScript predictably
Use page context for DOM work
Code such as chart initialization, hiding an interactive control, expanding a disclosure or replacing a live clock belongs in page.evaluate(). Return a small serializable result if the Node or Python process needs confirmation. Do not return DOM nodes or class instances; return strings, numbers, booleans or plain objects.
Use an init script for pre-load changes
If a site’s own JavaScript reads a value before your navigation completes, inject it before loading the page. In Puppeteer, call page.evaluateOnNewDocument(() => { ... }). This is useful for setting a deterministic timezone, stubbing a browser-only API or installing an early event listener. It is different from post-load evaluate(), which is appropriate after the document exists.
Make the page announce readiness
Prefer an application-owned signal over a fixed delay. For example, after the final API response is applied and the chart library has finished drawing, set window.__PDF_READY__ = true or add a data-pdf-ready="true" attribute to the report root. The automation can then wait for that exact condition. A timeout remains necessary as a safety limit, but it should not be the definition of completion.
Waiting for charts, fonts and asynchronous data
Charts and canvas output
Many chart libraries animate. Disable animation for export or expose a promise such as window.renderChartsForPdf() that resolves after the final frame. If a chart is drawn on a canvas, verify that the canvas dimensions are set before PDF generation; a zero-sized canvas can produce a blank area even when JavaScript ran successfully.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Data requests
Wait for the state transition that follows the last request, not merely for the network to become quiet. A page may open polling connections, analytics requests or image fetches after the report is visually complete. A selector such as [data-report-status="complete"] or an explicit readiness flag is more reliable.
Web fonts and images
Puppeteer’s documentation notes that PDF generation waits for fonts by default, but explicitly awaiting document.fonts.ready makes your intent clear and helps when fonts are loaded by application code. For images, wait for load or decode events and provide a failure path so one broken image does not hang the export forever.
Delays as a last resort
A short delay can accommodate a third-party widget with no readiness API, but it is inherently machine- and data-dependent. Keep it bounded and combine it with a selector or flag whenever possible.
Print CSS versus screen CSS
Both Puppeteer and Playwright use print media for PDFs by default. Define a print stylesheet for pagination and export-only changes:
Free tools Windows power users keep installed
One-click scans. No signup required.
@media print {
.toolbar, .live-chat, .no-print { display: none !important; }
.report { break-inside: avoid; }
}
@page {
size: A4;
margin: 18mm 14mm;
}
.chart, img { max-width: 100%; }
To reproduce the screen design instead, Puppeteer supports page.emulateMediaType('screen'); Playwright supports page.emulate_media(media='screen'). Print output colors may be adjusted for printing. Where supported, -webkit-print-color-adjust: exact requests the specified colors, although it can increase ink use and should be tested with your browser version.
PDF controls you should choose deliberately
| Decision | Available control | Why it matters |
|---|---|---|
| Paper | format such as A4 or Letter, or CSS @page |
Controls pagination and printable area. |
| Margins | Top, right, bottom and left values | Prevents headers, footers and content from colliding. |
| Backgrounds | printBackground: true |
Includes colored panels and chart fills that browsers otherwise omit. |
| Headers and footers | displayHeaderFooter, headerTemplate, footerTemplate |
Templates can include supported classes for date, title, URL and page number/total pages. |
| CSS page size | preferCSSPageSize: true |
Lets an authored @page rule take precedence over a format setting. |
| Orientation | Landscape option or a landscape @page rule |
Useful for wide tables and dashboards. |
Troubleshooting missing or incorrect content
The PDF contains the loading skeleton
Cause: navigation completed before the application’s data promise. Fix: expose a post-render flag or completion selector and wait for it with waitForFunction() or waitForSelector(). Increase the timeout only after confirming the page eventually reaches that state.
Charts are empty or half drawn
Cause: animation, zero dimensions or a canvas drawn before its container was laid out. Fix: disable export-time animation, force layout, await the chart library’s completion callback and verify canvas width and height in evaluate().
Fonts fall back
Cause: the font URL is inaccessible to the browser, the font request is still pending or the page uses a relative URL without a base origin. Fix: serve the font over a reachable URL, wait for document.fonts.ready, and inspect browser console and request failures.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Colors or backgrounds disappear
Cause: print media rules or the default background omission. Fix: add print-specific styles, set printBackground: true, and use print-color adjustment only when the output requires exact colors.
Headers overlap the report
Cause: header/footer templates occupy space that the content margin does not reserve. Fix: increase the corresponding PDF margin and keep templates simple; template CSS does not inherit the page’s normal stylesheet.
evaluate() throws a serialization or reference error
Cause: the callback references a Node/Python variable or returns a non-serializable browser object. Fix: pass plain arguments explicitly and return plain data. Keep filesystem, secrets and database calls in the host process, not in page JavaScript.
The export times out or hangs
Cause: an awaited request, image or readiness flag never resolves. Fix: add bounded waits, resolve image errors, log failed requests, and make the application set an explicit failure state that the host can report.
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 →Performance, reliability and cost considerations
Launching a browser is substantially heavier than manipulating an HTML string, so reuse a browser process and create a fresh page per job when your service handles multiple documents. Limit concurrency to what the host has memory and CPU for; validate the limit with your own documents because the cited documentation does not define a universal throughput or resource figure.
Cache browser binaries in deployment images, avoid waiting for analytics or advertising requests, and use a readiness signal instead of an unnecessarily long network-idle window. Record the URL, browser version, elapsed phases and failure reason for each job. For untrusted pages, isolate the browser, restrict network access where possible and never expose secrets through page globals.
There is no authoritative benchmark in the cited Puppeteer or Playwright documentation for accuracy, throughput or startup cost. Measure those values against your templates, fonts, chart libraries and deployment sandbox rather than applying a generic number.
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 that can return a PNG, JPEG, WebP or PDF from one GET request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; 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.
For a one-call capture, use the API base shown below (replace the URL with your page):
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}`);
Its options include custom JavaScript and CSS, waits for selectors, delays or network idle, full-page capture with lazy images loaded, PDF paper size, margins, landscape and page ranges, plus custom headers, cookies, user agents, authorization, timezone and geolocation. An MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. See the ScreenshotNeo documentation for request options and PDF configuration.
Rank #4
The Free plan includes 1,000 shots each month with no card. Paid plans are Starter $5 for 3,000 shots, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to start with 1,000 screenshots a month and no card.
FAQ
Can page JavaScript read files or environment variables during export?
No. Code evaluated in the browser has browser permissions. Read files, query databases and access environment variables in the host process, then pass only the required, non-sensitive data into the page.
PC 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 & 11Outdated 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 matchShould I use a PDF library instead of a browser?
Use a browser renderer when the source depends on layout, JavaScript, web fonts, canvas or CSS media rules. A non-browser PDF library can be appropriate for documents built from primitives, but it will not execute an application’s DOM code automatically.
How can I make failures diagnosable in a queue?
Save the document URL or template version, readiness state, browser console messages, failed-request URLs, elapsed navigation and render times, and the final exception. Retain a diagnostic screenshot or HTML snapshot only when your data-handling policy permits it.
Frequently Asked Questions
Can page JavaScript read files or environment variables during export?
No. Evaluated code runs in the browser context; perform file, database and environment access in the host process and pass only non-sensitive serializable data.
Should I use a PDF library instead of a browser?
Choose a browser renderer when JavaScript, DOM layout, web fonts, canvas or print CSS are required. Primitive, non-interactive documents may suit a non-browser library.
How can I make failures diagnosable in a queue?
Record the template or URL, readiness state, console and request failures, elapsed phases and the final exception; retain snapshots only if your data policy allows them.
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.

