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

Run the HTML in Chromium, make sure the external JavaScript has loaded and finished rendering its content, then generate the PDF with Puppeteer’s page.pdf() or Playwright’s PDF API. Loading a script and completing the page’s application rendering are separate events, so wait for a page-specific ready condition rather than relying only on a network-idle state or fixed delay.

Why external JavaScript can be missing from a PDF

A PDF converter must execute the HTML in a browser context for JavaScript to change the page that gets printed. A converter that merely reads the HTML source will not run a browser script or include content that the script adds later.

Even in Chromium, a successful script download does not prove that the script finished its work. It may fetch data, render a chart, or update the DOM asynchronously after the file itself has loaded. Capture the PDF only after the page reaches a meaningful, application-specific ready state.

Load the script and create a PDF with Puppeteer

Install Puppeteer in your Node.js project, then use this ES module pattern. Replace the example page, script URL and readiness condition with the values for your application.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();

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

  page.on('console', message => {
    console.log('PAGE LOG:', message.text());
  });
  page.on('pageerror', error => {
    console.error('PAGE ERROR:', error);
  });
  page.on('requestfailed', request => {
    console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText);
  });
  page.on('response', response => {
    if (response.status() >= 400) {
      console.error('HTTP ERROR:', response.status(), response.url());
    }
  });

  await page.goto('https://example.com/report.html', {
    waitUntil: 'networkidle2'
  });

  // Add this only if the document does not already load the script.
  await page.addScriptTag({
    url: 'https://cdn.example.com/report.js'
  });

  // Use a real signal set after the application has rendered its content.
  await page.waitForFunction(() => window.reportReady === true);

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

The example assumes the report page sets window.reportReady = true only after it has finished rendering. If it does not expose a flag, wait for a stable element that appears only when the output is ready, such as a rendered report container or chart. A selector is useful only if its presence really means the content is complete.

Let the document load its own script when possible

If the HTML already contains a <script src="…"> reference, do not inject the same file a second time. Duplicate execution can repeat event handlers or application work. Use page.addScriptTag({ url }) when the document does not reference the dependency, or when your conversion process deliberately adds it after navigation.

Choose a readiness signal, not just a delay

waitUntil: 'networkidle2' is a useful navigation condition, but it describes network activity, not whether the application has completed rendering. A page can become quiet before a delayed task runs, or keep making requests after the content you need is ready. Puppeteer also documents waitForNetworkIdle; treat network-idle waiting as a coarse aid, then use a page-specific assertion for the final capture condition.

A fixed wait such as setTimeout may appear to solve a timing problem, but it can be too short on a slow run and waste time on a fast one. Prefer a readiness flag or selector. If the application provides no such signal, add one to the page’s own code when you can; otherwise, define and verify a concrete DOM condition that reliably represents the finished output.

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

Generate the PDF with Playwright instead

Playwright uses the same basic approach: navigate in a real browser, wait for the rendered page, then call its PDF API. Its navigation options include load, domcontentloaded, networkidle and commit. The network-idle state is a navigation aid, not proof that application rendering has ended; Playwright labels networkidle as discouraged for testing.

import { chromium } from 'playwright';

const browser = await chromium.launch();

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

  page.on('console', message => {
    console.log('PAGE LOG:', message.text());
  });
  page.on('pageerror', error => {
    console.error('PAGE ERROR:', error);
  });
  page.on('requestfailed', request => {
    console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText);
  });

  await page.goto('https://example.com/report.html', {
    waitUntil: 'load'
  });

  // Add only when the HTML does not already reference this file.
  await page.addScriptTag({
    url: 'https://cdn.example.com/report.js'
  });

  await page.waitForFunction(() => window.reportReady === true);

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

Choose between Puppeteer and Playwright based on the browser versions, isolation, fixtures and operational tooling already used by your project. Both support the essential workflow of browser navigation, page readiness and PDF generation.

Make the PDF match the intended page

Print media versus screen media

Puppeteer’s page.pdf() uses print CSS media by default. That is appropriate for a document designed for printing, but it can produce a different layout from the page shown on screen. For screen styling, set the media type before generating the PDF:

await page.emulateMediaType('screen');
await page.pdf({ path: 'report.pdf', printBackground: true });

Use print media when the page has dedicated print styles; use screen media when the on-screen design is what you need to preserve. Check the CSS in both cases: page breaks, hidden elements and responsive rules can change what appears in the output.

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

Fonts, colors and backgrounds

Puppeteer’s PDF generation waits for fonts by default; its API also provides waitForFonts when you need to set that behavior explicitly. If text wraps differently or pagination shifts, verify that the intended web fonts loaded before capture.

Printing can alter colors. When exact CSS colors matter, the page’s print styles can use -webkit-print-color-adjust. Set printBackground: true when the PDF needs background graphics, as in the examples above. Confirm that this matches the target design rather than assuming the default print output will include screen backgrounds.

Troubleshoot missing or incomplete JavaScript output

  • The script request fails: Check the browser-side response and request-failure logs, then verify that the URL is reachable from the machine running Chromium. A URL that works on your workstation may not be reachable from a server or container.
  • The script returns an error status: Inspect the response status and script URL. Confirm the path, CDN availability, and any required authentication or cookies.
  • The script loads but content is absent: Check page console messages and page errors. Verify that the script runs in the same page or frame being printed, and wait for a real application-ready flag or rendered selector.
  • The page uses a content security policy: CSP can prevent an injected script from running. Check the browser console and the site’s policy; use the page’s approved script-loading method or adjust the policy if you control the page.
  • Cross-origin, mixed-content or authentication rules interfere: Confirm that the browser can load the resource under the page’s security rules, that HTTPS content is not trying to load an insecure script, and that the necessary headers or cookies are present.
  • A timeout occurs with network-idle waiting: Some pages keep connections active or make continuing requests. Use an appropriate navigation state, then wait for the page-specific content condition instead of making network idleness the sole gate.
  • The output differs from the browser view: Check whether the PDF uses print or screen media, whether fonts have loaded, whether background printing is enabled, and whether viewport-dependent CSS changes the layout.
  • The process leaves browser resources open: Close the browser after the PDF file or buffer has been produced. Put cleanup in a finally block so it also runs when navigation or rendering throws an error.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability considerations

Browser rendering adds work compared with simply transforming static markup, but it is what allows page JavaScript, layout, fonts and print CSS to affect the PDF. Reusing a browser process for multiple jobs can avoid repeatedly launching Chromium, but isolate pages and close each page when its job is complete; always close the browser when the worker shuts down. If jobs run concurrently, size concurrency for the memory and CPU available to the host rather than assuming each render is lightweight.

Reliability depends on the resources the page needs. External scripts, fonts and data endpoints must be reachable from the browser process, and dynamic content must expose a dependable completion signal. Record console errors, failed requests and unsuccessful HTTP responses while diagnosing failures; remove or reduce verbose logging once the pipeline is stable. Network-idle navigation can help with timing, but it is not a substitute for application-level readiness.

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

Or skip the browser setup

If you need a screenshot or PDF from a URL without operating Puppeteer or Playwright yourself, ScreenshotNeo is a website screenshot API and MCP server. Its API returns a PNG, JPEG, WebP or PDF from one GET request. This is a URL-based capture alternative; it does not replace custom Node.js page logic or guarantee that arbitrary application-specific JavaScript has completed. See the ScreenshotNeo API documentation for parameters and response details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie or consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots, and yearly billing gives two months free. Every feature is available on every plan.

Sign up for ScreenshotNeo free to get 1,000 screenshots a month with no card.

Frequently Asked Questions

Does adding a script tag wait for the script’s asynchronous rendering work?

No. It loads the script, but the page can continue rendering afterward. Wait for an application-specific readiness condition before creating the PDF.

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

Can I use a local HTML file with this approach?

Yes. Navigate Chromium to the local file from the process that runs the browser, then ensure its external script and other required resources are reachable in that browser context.

Should I use Puppeteer or Playwright for a new PDF job?

Either supports browser rendering and PDF generation. Prefer the library that best fits the browser versions and operational tooling already used by your project.

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.