October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
Chromium

How to Load External JavaScript When Converting HTML to PDF in Node.js

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a real Chromium page, not a string-to-PDF shortcut. Open the HTML with Puppeteer or Playwright, let the browser fetch and execute the external JavaScript, wait for a deterministic application-ready signal, and only then call the PDF API. A network-idle event alone can occur before your charts, tables, or client-side components finish rendering.

The reliable rendering sequence

An HTML-to-PDF converter must execute the document in a browser context for an external script to change the printed output. The dependable sequence is:

  1. Launch Chromium and create an isolated page.
  2. Navigate to the HTML, or set the HTML directly.
  3. Allow the page’s own <script src="…"> dependency to load, or inject the file with Puppeteer’s addScriptTag when the document has no script tag.
  4. Wait for a page-specific marker such as a rendered selector or window.reportReady === true.
  5. Set the intended media type and print options.
  6. Generate the PDF, then close the browser.

Puppeteer’s PDF guide uses navigation with waitUntil: 'networkidle2' before saving a PDF, and its API waits for fonts by default. Treat network idle as a navigation aid, not proof that application rendering is complete. Playwright exposes equivalent load, domcontentloaded, networkidle, and commit states; its documentation describes network idle as discouraged for tests, which is another reason to add an application-level readiness check.

See the Puppeteer PDF generation guide, Puppeteer addScriptTag API, and Playwright page navigation documentation for the current API details.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer: complete Node.js example

Install Puppeteer in the project that will perform the conversion:

npm install puppeteer

This example assumes report.html already references its external file. The page sets a readiness flag after the JavaScript has finished rendering.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();

  await page.goto('file:///absolute/path/to/report.html', {
    waitUntil: 'networkidle2'
  });

  await page.waitForFunction(() => window.reportReady === true, {
    timeout: 30000
  });

  // PDF defaults to print media. Keep this line only when the stylesheet
  // should be evaluated as it is on screen.
  await page.emulateMediaType('screen');

  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true
  });
} finally {
  await browser.close();
}

A minimal page-side readiness implementation might look like this:

<script src="https://cdn.example.com/report.js"></script>
<script>
  renderReport().then(() => {
    window.reportReady = true;
  });
</script>

If the script itself creates the final DOM, set the flag at the end of that script or expose a more meaningful condition, such as the presence of a populated chart container. A fixed setTimeout is less reliable because it is either unnecessarily slow on fast runs or too short under load.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Injecting a script that is absent from the HTML

Use addScriptTag only when the document does not already include the dependency. Adding the same library twice can register duplicate handlers, overwrite state, or render the component twice.

await page.goto('file:///absolute/path/to/report.html', {
  waitUntil: 'networkidle2'
});

await page.addScriptTag({
  url: 'https://cdn.example.com/report.js'
});

await page.waitForFunction(() =>
  document.querySelector('[data-report-rendered="true"]') !== null
);

The URL must be reachable from the Chromium process. A successful HTTP response does not guarantee useful output: the file might be an error page, an incompatible module, or a script blocked by policy. Check the browser’s response and console events while diagnosing.

Make readiness observable instead of guessing

Wait for a rendered selector

A selector is appropriate when your application adds a stable element after rendering:

await page.waitForSelector('#report-table tbody tr', {
  visible: true,
  timeout: 30000
});

For a chart, wait for the SVG, canvas wrapper, or a class that your own code adds after data and layout work complete. Avoid selecting an element that exists in the initial empty template.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for an application flag

An explicit flag works well for several asynchronous operations:

await page.waitForFunction(
  () => window.reportReady === true,
  { timeout: 30000 }
);

Set the flag only after data requests, image decoding, chart drawing, and any post-processing that affects the PDF have completed. If a promise can fail, set a separate error flag and reject visibly rather than leaving the converter waiting until timeout.

Handle frames deliberately

JavaScript running inside an iframe changes that frame’s DOM, not the top-level page. Obtain the correct frame and wait there, or ensure the iframe exposes a message or marker that the top-level page can observe. Printing the parent page will not automatically turn an inaccessible cross-origin frame into editable HTML.

External-script failures and browser policies

  • Content Security Policy: a script-src policy can reject a CDN or an injected script. Adjust the policy, host the approved asset, or use a nonce/hash designed for that document.
  • Authentication: private scripts need headers, cookies, or an authenticated page session. Configure those before navigation; do not put secrets in a public script URL.
  • Mixed content: an HTTPS page can block an HTTP script. Use HTTPS for both, or serve the asset from the same secure origin.
  • CORS and module rules: classic scripts and ES modules have different loading requirements. A module may need an appropriate MIME type and CORS response.
  • CDN or DNS availability: the browser process needs outbound network access and a resolvable hostname. A developer laptop succeeding does not prove a restricted worker or container can reach it.
  • Wrong execution context: a script loaded in one page or frame cannot render a different page being printed. Attach it and inspect it in the same target that calls page.pdf().

PDF fidelity: media, fonts, colors, and layout

Puppeteer evaluates print CSS for page.pdf() by default. If the design was authored for screen media, call await page.emulateMediaType('screen') immediately before PDF generation, as documented in the Puppeteer API. Conversely, remove that call when print-specific rules are intentional.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Printing can alter colors. For brand colors or shaded table cells, add -webkit-print-color-adjust: exact; to the relevant rules and keep printBackground: true. This requests color preservation; it does not override every browser or operating-system policy.

Puppeteer says PDF generation waits for fonts by default. You can make the dependency explicit when diagnosing pagination:

await page.evaluate(async () => {
  if (document.fonts) await document.fonts.ready;
});

Font metrics affect line wrapping and therefore page breaks. Wait for web fonts before capturing, use stable fallbacks, and test with the same fonts available in production. Prefer CSS @page rules when the document owns its paper size:

@page {
  size: A4;
  margin: 16mm;
}

In code, preferCSSPageSize: true lets those rules take precedence. Otherwise specify format, width, height, and margins in the PDF options. Remember that screen viewport width still controls responsive breakpoints; set it deliberately with page.setViewportSize (Playwright) or page.setViewport (Puppeteer) before navigation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Playwright alternative

Playwright uses the same browser-rendering model and offers Chromium, Firefox, and WebKit automation. Install it with:

npm install playwright
npx playwright install chromium

A Chromium PDF conversion with a selector-based readiness check is:

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
  await page.goto('https://example.com/report.html', {
    waitUntil: 'networkidle'
  });
  await page.waitForSelector('[data-report-rendered="true"]', {
    state: 'visible',
    timeout: 30000
  });
  await page.emulateMedia({ media: 'screen' });
  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true
  });
} finally {
  await browser.close();
}

Choose the library already supported by your project, fixtures, browser versions, and operational tooling. Both execute external JavaScript in Chromium and expose navigation, readiness, and PDF primitives; switching libraries does not remove the need for a deterministic application condition.

Diagnostics: capture the evidence the browser sees

Add listeners before navigation so a failed load cannot disappear from the log:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.on('console', msg => console.log('[console]', msg.type(), msg.text()));
page.on('pageerror', err => console.error('[pageerror]', err));
page.on('requestfailed', req => console.error('[requestfailed]', req.url(), req.failure()));
page.on('response', res => {
  if (res.status() >= 400) console.error('[response]', res.status(), res.url());
});

Then verify the exact script URL, status code, content type, and execution errors in the output. If the page is local, use an HTTP server rather than relying on assumptions about file:// origin behavior:

npx http-server ./public -p 8080

Navigate to http://127.0.0.1:8080/report.html and keep the server alive for every asset request.

Common symptoms and precise fixes

Symptom Likely cause Fix
PDF contains the template but no chart or table Capture ran before asynchronous rendering Wait for a rendered selector or application-ready flag; do not rely only on a delay.
addScriptTag rejects Unreachable URL, CSP, mixed content, or invalid response Inspect console, failed requests, and response status; correct policy, protocol, credentials, or URL.
Script appears loaded but has no effect Wrong frame, duplicate library, module mismatch, or runtime exception Check the executing frame, browser console, script type/MIME, and page errors.
PDF colors differ from the browser Print media and background/color adjustment rules Choose print or screen media intentionally, enable printBackground, and use -webkit-print-color-adjust: exact where needed.
Unexpected page breaks or clipped text Fonts not ready, viewport differs, or print CSS changes dimensions Await document.fonts.ready, set the viewport, and inspect @page, margins, and print rules.
Navigation times out Long polling, blocked resource, or genuinely slow dependency Set a justified navigation timeout, block nonessential resources only when safe, and use a page-specific readiness check with a separate timeout.
Worker process hangs or leaks memory Browser not closed after an exception Put PDF generation in try/finally and always call browser.close().
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability, performance, and cost decisions

Use bounded waits

Set separate limits for navigation and application readiness. A page can finish navigation quickly while its API call fails, so a single generous timeout hides the real cause. Log the URL, navigation state, readiness condition, and elapsed time for each job.

Reuse browsers carefully

Launching Chromium for every document is slower, but sharing a browser across jobs reduces startup cost. Use a fresh incognito context or page per job, clear sensitive cookies, and close pages after the PDF buffer is written. Cap concurrency according to available CPU and memory; excessive parallel pages make JavaScript and font work contend and increase timeouts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Control nondeterminism

Pin browser and dependency versions where reproducibility matters. Freeze or mock volatile API data, provide all required fonts, and avoid animations (for example, disable transitions in a print-only stylesheet). Capture after images report decoded dimensions, not merely after their network responses.

Keep output and errors separate

Write the PDF only after readiness succeeds. On failure, retain console and request logs and return a structured error; never publish a partially rendered file as if it were complete.

Or skip the browser setup

ScreenshotNeo is a managed website screenshot API and MCP server. It is useful when you need a clean rendered capture without operating Chromium yourself. Before capture it accepts cookie/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 are not billed, and response headers report the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes the features; the Free plan provides 1,000 shots per month without a card, and paid plans start at $5 for 3,000 shots.

For an HTML page already reachable by URL, the one-call request is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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 output and option details. The API supports PNG, JPEG, WebP, or PDF and options including full-page lazy-image loading, CSS-selector element capture, device presets and custom viewports, dark mode, retina scale, print settings, custom CSS/JavaScript, clicks, hidden selectors, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification.

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(`ScreenshotNeo HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

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)

Start with the free ScreenshotNeo sign-up: 1,000 screenshots each month, no card required.

FAQ

Does loading a script mean the PDF is ready?

No. The file can be downloaded and executed while its data requests, layout, fonts, or canvas drawing are still pending. Wait for your application’s explicit completion condition.

Should I use Puppeteer or Playwright?

Either is suitable for this browser-rendering pattern. Base the choice on the browser versions, fixtures, isolation model, and deployment tooling your project already uses.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does a screen-looking page print differently?

PDF generation evaluates print CSS by default, and fonts, viewport breakpoints, backgrounds, and color-adjustment rules can all change pagination or appearance. Select the media type and print options deliberately.

Can a PDF converter execute JavaScript from a private CDN?

Yes, if the browser process can authenticate and reach it and the page’s CSP, origin, protocol, and module rules permit execution. Configure the session before navigation and inspect failed requests when it does not load.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.