What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
In Puppeteer, navigate to the document, wait for the page state you need, then await page.addStyleTag({ url: cssUrl }) before calling page.pdf(). Select the correct media type, enable background printing, and let Chromium finish fonts and other remote assets. The same sequence works in Playwright.
The reliable sequence
A PDF captures the page as Chromium sees it at render time. A remote stylesheet must therefore be reachable from the browser process, attached to the document, and loaded before PDF generation.
- Open the HTML. Use
page.goto()with an explicit wait condition such aswaitUntil: 'networkidle2'for a remote page. - Attach the URL stylesheet. Call
await page.addStyleTag({ url: cssUrl }). Puppeteer adds a<link rel="stylesheet">element and resolves the promise after the stylesheet has loaded or the CSS content has been injected. - Choose the media type.
page.pdf()uses print CSS media by default. Keep that default for print-specific styles, or callawait page.emulateMediaType('screen')when the screen rules are the ones you need. - Render with the required print options. Set
printBackground: truefor background colors and images. SetpreferCSSPageSize: truewhen the stylesheet contains an@pagesize that should override the PDF format or dimensions. - Close the browser. Put cleanup in a
finallyblock so failed captures do not leave Chromium processes running.
Complete Puppeteer example
This example loads an invoice page, injects a second stylesheet from a CDN, uses screen media, and writes an A4 PDF. Remove the emulateMediaType call if the stylesheet is intentionally written for print.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
page.on('requestfailed', request => {
console.error('Request failed:', request.url(), request.failure()?.errorText);
});
page.on('console', message => {
if (message.type() === 'error') console.error('Page error:', message.text());
});
await page.goto('https://example.com/invoice.html', {
waitUntil: 'networkidle2'
});
await page.addStyleTag({
url: 'https://cdn.example.com/print.css'
});
// Optional: use screen rules instead of the default print rules.
await page.emulateMediaType('screen');
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
waitForFonts: true,
timeout: 60000
});
} finally {
await browser.close();
}
The waitForFonts and timeout settings are useful when fonts are slow or managed by application code. Puppeteer’s PDF API waits for fonts by default, but an explicit timeout gives a bounded failure instead of allowing a stuck asset to hold a job indefinitely.
#1 Best Overall
When the page already contains a stylesheet link
If invoice.html already has <link rel="stylesheet" href="https://cdn.example.com/print.css">, do not inject a duplicate link. Navigate first and wait for the navigation condition, then generate the PDF. Adding a second copy can make debugging harder and can change cascade order when the two links contain overlapping rules.
Use addStyleTag({ url }) when the CSS URL is chosen at runtime, when you need to add a print override to an existing page, or when the HTML is controlled by another system.
Print media, screen media, and page sizing
Print media is the default
Puppeteer documents PDF generation as using the print CSS media type. Rules inside @media screen are therefore not applied unless you explicitly select screen media. A common symptom is a PDF that is structurally correct but lacks the layout, colors, or spacing visible in a normal browser tab.
await page.emulateMediaType('screen');
await page.pdf({
path: 'screen-layout.pdf',
printBackground: true
});
Backgrounds require an explicit option
Chromium can omit background graphics during printing. Set printBackground: true when your design depends on colored panels, background images, or full-bleed sections. This option does not repair a missing stylesheet; it only controls whether backgrounds that are already applied are painted into the PDF.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated 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 matchRank #2
Let CSS control the paper size
If the stylesheet defines an @page rule, preferCSSPageSize: true gives that CSS size priority over format, width, or height values supplied to page.pdf(). If you need a predictable standard sheet regardless of CSS, use a format such as 'A4' and leave that preference disabled.
Why a URL stylesheet is still missing
The stylesheet was injected during a navigation race
Calling page.pdf() immediately after page.goto() can race the document and its assets. Await the navigation promise, then await page.addStyleTag(). If the page is a single-page application, also wait for a selector that proves the application has mounted before injecting CSS.
The media type does not match the rules
Inspect the stylesheet for @media print, @media screen, or unqualified rules. A screen-only stylesheet can load successfully and still appear to be absent in a print PDF. Select screen media deliberately, or move the required declarations into print-compatible rules.
Chromium cannot reach the CSS or its dependencies
The browser must be able to fetch the CSS URL, follow redirects, download fonts and images, and resolve nested @import resources from its own network context. Authentication requirements, a restrictive content-security policy, DNS or TLS failures, and blocked requests can leave all or part of the stylesheet unapplied.
Rank #3
Use the requestfailed and console listeners shown in the example. Check the exact URL reported by the failed request, then test that URL from the same runtime, with the same proxy, credentials, and certificate configuration. A successful request from your laptop does not prove that the server running Chromium has the same access.
Fonts arrive after the first paint
Font files are separate network resources. A PDF can contain the correct layout but use fallback glyphs when a webfont is late or blocked. Keep font waiting enabled, increase the PDF timeout for slow environments, and make sure the font URLs are reachable from the browser process. If the application swaps fonts after a client-side event, wait for that event or for a selector that represents the finished state before rendering.
The PDF options hide what you expect to see
Missing backgrounds usually indicate printBackground, while a page that ignores an @page declaration usually indicates preferCSSPageSize. Neither option changes the CSS cascade, so fix media selection and resource loading separately.
Diagnosing the loaded stylesheet in the page
For difficult cases, inspect the browser’s style sheets after injection. This confirms whether the link element exists and whether the browser can read its rules.
Rank #4
const sheets = await page.evaluate(() =>
[...document.styleSheets].map(sheet => ({
href: sheet.href,
ruleCount: (() => {
try { return sheet.cssRules.length; }
catch { return null; }
})()
}))
);
console.table(sheets);
A non-null href with a null rule count commonly means the stylesheet is cross-origin and the browser does not expose its rules to page JavaScript. That does not by itself prove the CSS failed to apply; use request failures, console errors, and the rendered result as well.
Playwright equivalent
Playwright exposes the same basic strategy with its own browser launcher and media API. Its page API accepts a stylesheet URL, and its PDF method uses print media unless you select screen media.
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com/invoice.html', {
waitUntil: 'networkidle'
});
await page.addStyleTag({
url: 'https://cdn.example.com/print.css'
});
await page.emulateMedia({ media: 'screen' });
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
printBackground: true
});
} finally {
await browser.close();
}
Choose Puppeteer when its API and Chromium management fit your existing Node.js service; choose Playwright when its browser and context tooling better fit your test or rendering system. For this task, both libraries require the same decisions: navigation waiting, stylesheet reachability, print-versus-screen media, authentication or interception, and deterministic fonts and assets.
Reliability, performance, and operating cost
Make waiting deterministic
Use the narrowest condition that represents a complete document. A network-idle condition is useful for a mostly static page, while an application that keeps telemetry or streaming connections may need an explicit ready selector or application event. Waiting longer than necessary increases latency; waiting for too little produces intermittent PDFs.
Keep browser versions and assets controlled
Chromium is part of the rendering stack, so browser-version management affects CSS support, font shaping, and pagination. Pin the browser/runtime combination used in production, serve fonts and CSS from stable URLs, and log the target URL and failed requests for each job.
Budget for a real browser
Puppeteer and Playwright run Chromium, so memory, startup time, concurrency limits, and sandbox configuration are operational costs. The cited API documentation describes stylesheet and PDF behavior, not throughput or reliability benchmarks; measure those properties under your own page sizes, asset counts, and concurrency.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| CSS works in the browser but not in the PDF | Print media is active and the rules are screen-only | Call emulateMediaType('screen'), or provide print rules. |
| Only colors and images are missing | Background printing is disabled | Set printBackground: true. |
| Injected CSS sometimes appears | The link or its dependencies are still loading | Await navigation and addStyleTag(); then wait for a ready selector if the app is dynamic. |
| Request failures mention the CDN, fonts, or imports | Chromium lacks network access, credentials, or permission | Fix runtime DNS, TLS, proxy, authentication, CSP, or request blocking and inspect requestfailed. |
Paper size ignores @page |
PDF dimensions take precedence | Set preferCSSPageSize: true, or remove the CSS size rule when a fixed format is required. |
| Fallback fonts or a timeout | Fonts are slow, blocked, or swapped after load | Keep font waiting enabled, increase the timeout, and wait for the application’s final font state. |
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It accepts a URL and returns a clean PNG, JPEG, WebP, or PDF. Before capture, it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response reports its page verdict and billing status in X-Page-Verdict and X-Billed headers.
For a Node.js call, follow the current request options in the ScreenshotNeo documentation:
Recommended Free Tools
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/invoice.html' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The same endpoint can be called from cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/invoice.html -o shot.webp
Or from Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/invoice.html"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Useful capture controls
- Full-page capture loads lazy images; you can also capture one element by CSS selector.
- Choose dark mode, any viewport, one of 12 device presets, and retina scale.
- For PDFs, set paper size, margins, landscape orientation, and page ranges.
- Supply custom CSS or JavaScript, click an element before capture, hide selectors, or wait for a selector, a delay, or network idle.
- Block ads, trackers, requests, or resource types; send custom headers, cookies, user agents, or Authorization values; and set timezone or geolocation.
- Use transparent backgrounds, image resizing, a chosen cache TTL, signed links for public
<img>tags, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, or the OpenAPI specification. - The parameter names used by other screenshot APIs also work, which can simplify migration.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients.
Plans
| Plan | Allowance | Price |
|---|---|---|
| Free | 1,000 shots/month | Free, no card |
| Starter | 3,000 shots | $5 |
| Growth | 15,000 shots | $15 |
| Pro | 60,000 shots | $39 |
| Scale | 250,000 shots | $99 |
| Business | 1,000,000 shots | $249 |
Yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
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.

