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

Use a real browser engine when JavaScript must run before the PDF is created. In PHP, Spatie Browsershot is usually the simplest integration: it sends a URL or HTML string to Puppeteer, which controls headless Chrome. For a lower-level PHP API, use chrome-php/chrome; for a shell-based fallback, invoke Chrome’s headless command. PHP-only libraries such as Dompdf cannot execute JavaScript, so they cannot reproduce a page whose content is created or changed in the browser.

Why JavaScript changes the PDF approach

A static HTML-to-PDF library parses markup and CSS inside PHP. A JavaScript application is different: the initial response may contain only a shell, while scripts fetch data, render components, apply styles, and insert the final DOM. A browser-backed renderer performs those steps before printing.

  • Browser-backed rendering: Chrome/Chromium executes scripts, loads browser-compatible CSS, and prints the resulting page.
  • PHP-only rendering: a library such as Dompdf can be suitable for server-rendered invoices or reports, but its tutorial states that “Dompdf does not run JavaScript.”
  • Legacy WebKit rendering: wkhtmltopdf can work for existing, simple deployments, but its Qt WebKit engine differs from current Chrome. Test modern CSS, fonts, and JavaScript-dependent layouts before adopting it.

The practical rule is simple: if the visible content depends on JavaScript, use a browser process and wait for the page’s actual ready condition before printing.

Choose a renderer for your PHP deployment

Option JavaScript PHP integration Best fit Main trade-off
Spatie Browsershot Yes, through Puppeteer and headless Chrome High-level PHP/Laravel-friendly API Most applications that need URLs, HTML strings, waiting, and PDF options Requires Node.js, Puppeteer, and a Chrome/Chromium executable
chrome-php/chrome Yes Direct PHP control of Chrome/Chromium PHP-first services that need low-level navigation and evaluation You manage browser processes and more implementation details
Chrome headless CLI Yes PHP launches an operating-system process Small workers, cron jobs, or environments already standardized on Chrome Waiting and interaction controls are less expressive than Puppeteer
Dompdf No PHP only Static, server-rendered HTML with supported CSS Cannot render JavaScript-created content
wkhtmltopdf Limited by its older WebKit engine PHP invokes an external binary Existing systems and simpler pages already validated with it Rendering can differ from current Chrome

The rest of this guide uses Browsershot first, then shows direct PHP and command-line alternatives.

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

Convert a JavaScript page with Spatie Browsershot

1. Install the dependencies

In a project that already has PHP and Composer, install Browsershot. Its Puppeteer layer needs Node.js and a browser installation.

composer require spatie/browsershot
npm install puppeteer

On a server, make sure the user running PHP can execute Node and Chrome/Chromium, and that outbound DNS, HTTPS certificates, fonts, and any authenticated endpoints are available. Pin package and browser versions in your deployment image so a browser update does not silently change layout.

2. Capture a URL

This is the smallest complete example. A filename ending in .pdf causes Browsershot to produce a PDF.

<?php
require __DIR__ . '/vendor/autoload.php';

use SpatieBrowsershotBrowsershot;

Browsershot::url('https://example.com')
    ->save(__DIR__ . '/output/example.pdf');

Use an absolute, writable path. If the page redirects, requires a login, or builds content asynchronously, add the relevant browser options before saving.

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

3. Render raw HTML that contains JavaScript

Use html() when PHP creates the document itself. The script below changes the DOM; the delay gives it time to run before Chrome prints.

<?php
require __DIR__ . '/vendor/autoload.php';

use SpatieBrowsershotBrowsershot;

$html = <<<'HTML'
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>body { font-family: sans-serif; }</style>
</head>
<body>
  <h1>Report</h1>
  <div id="result">Loading…</div>
  <script>
    document.querySelector('#result').textContent = 'Rendered in Chrome';
  </script>
</body>
</html>
HTML;

Browsershot::html($html)
    ->delay(500)
    ->showBackground()
    ->format('A4')
    ->save(__DIR__ . '/output/report.pdf');

For untrusted HTML, isolate the browser and do not allow arbitrary scripts to access internal services. Treat injected HTML as executable code, not as harmless text.

4. Wait for the real ready condition

A fixed delay is only a fallback. It may be too short for a slow API or waste time on a fast page. Prefer a condition that represents completion:

<?php
use SpatieBrowsershotBrowsershot;

Browsershot::url('https://app.example.test/report/42')
    ->waitForSelector('#report-ready')
    ->waitUntilNetworkIdle()
    ->showBackground()
    ->margins(12, 12, 12, 12)
    ->save(__DIR__ . '/output/report-42.pdf');

Choose one or combine them according to the application:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Selector wait: have the application add a marker such as #report-ready only after data and charts are complete.
  • Network-idle wait: useful when the page finishes through a finite set of requests.
  • Short delay: useful for a known animation or a small client-side calculation.
  • Application signal: a deterministic “ready” element is safer than guessing a global timeout.

Do not assume that “network idle” means that a chart, web font, animation, or delayed timer is visually finished. Validate the actual PDF.

5. Control paper, orientation, and PDF output

Browser printing has independent layout and output settings. Typical Browsershot controls include:

<?php
Browsershot::url('https://example.com/invoice/42')
    ->format('A4')
    ->landscape()
    ->margins(8, 8, 8, 8)
    ->showBackground()
    ->hideBrowserHeaderAndFooter()
    ->save(__DIR__ . '/output/invoice.pdf');

Set margins in the same units your integration documents, enable background graphics when colors or charts matter, and disable browser-generated date, URL, and page-number decorations when they are not wanted. For a response rather than a file, Browsershot also documents PDF-saving and Base64-PDF methods; return the resulting bytes with the correct application/pdf content type.

6. Inspect the post-JavaScript DOM

When a PDF is empty or missing a component, first determine whether the browser ever produced that component. Browsershot’s bodyHtml() can retrieve the body after JavaScript has run. Save that diagnostic HTML during development and check selectors, API responses, and error messages before changing PDF settings.

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

Direct PHP control with chrome-php/chrome

Use chrome-php/chrome when you want a PHP API without the Browsershot abstraction. Its README lists PHP 7.4–8.5 and Chrome/Chromium 65+ as requirements.

composer require chrome-php/chrome
<?php
require __DIR__ . '/vendor/autoload.php';

use HeadlessChromiumBrowserFactory;

$factory = new BrowserFactory('/usr/bin/google-chrome');
$browser = $factory->createBrowser([
    'headless' => true,
]);

try {
    $page = $browser->createPage();
    $page->navigate('https://example.com/dashboard')->waitForNavigation();

    // Execute page JavaScript before printing.
    $page->evaluate("document.body.dataset.pdfRun = '1';")->getReturnValue();

    $page->pdf([
        'printBackground' => true,
        'landscape' => false,
    ])->saveToFile(__DIR__ . '/output/dashboard.pdf');
} finally {
    $browser->close();
}

For production, add an application-specific readiness check rather than assuming that navigation completion means that every asynchronous request is done. Also configure the executable path for the image or VM in which the code runs.

Use Chrome’s headless command from PHP

If your deployment already manages Chrome, the official command-line mode is a dependable fallback:

chrome --headless --print-to-pdf=output.pdf https://example.com

Chrome executes page code while constructing the DOM. You can cap the wait and remove generated decorations:

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.
chrome --headless --timeout=5000 
  --no-pdf-header-footer 
  --print-to-pdf=output.pdf 
  https://example.com

From PHP, use a process API, pass arguments as an array where possible, capture stderr, and enforce your own process timeout:

<?php
$command = [
    'chrome',
    '--headless',
    '--timeout=5000',
    '--no-pdf-header-footer',
    '--print-to-pdf=/tmp/page.pdf',
    'https://example.com',
];

$descriptors = [
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptors, $pipes);
if (!is_resource($process)) {
    throw new RuntimeException('Could not start Chrome');
}
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
if ($status !== 0 || !is_file('/tmp/page.pdf')) {
    throw new RuntimeException("Chrome failed: $stderr");
}

Use an argument-array process library in your own application if available; it avoids shell quoting and injection problems when URLs or file names contain user input.

Make JavaScript rendering reliable in production

Prepare deterministic input

  • Use absolute asset URLs or a stable base URL when injecting HTML.
  • Wait for fonts, images, charts, and API calls that affect the printed result.
  • Provide print CSS with @page, explicit widths, and print-specific visibility rules.
  • Use a fixed timezone, locale, and test data when PDF output must be reproducible.

Control the browser environment

  • Install the same Chrome/Chromium major version in development, CI, and production.
  • Bundle required fonts and confirm that the runtime user can read them.
  • Install CA certificates and verify outbound access to every API and asset host.
  • Run the browser with the sandbox policy required by your container; if a platform forbids the sandbox, isolate that worker instead of blindly adding insecure flags.
  • Close every browser process, cap concurrent jobs, and clean temporary profiles and PDF files.

Measure the right things

No authoritative comparative speed, memory, adoption, or success-rate figure establishes one renderer as universally fastest. Measure your own pages: navigation time, readiness wait, PDF generation time, peak memory, file size, and failure rate in the target image. Browser-backed rendering costs more operationally than a PHP-only library because it starts and manages a browser process, but it is the appropriate cost when JavaScript fidelity is required.

Common failures and fixes

Symptom Likely cause Fix
PDF contains only a loading shell Capture occurred before asynchronous rendering completed Wait for a ready selector or a known application signal; use a delay only when deterministic.
JavaScript errors in the page Missing environment variables, blocked API, CORS/authentication failure, or unsupported browser assumptions Inspect page console/network logs, make the endpoint reachable from the worker, and reproduce with the same URL and credentials.
“Chrome executable not found” Browser is absent or the configured path is wrong Install Chrome/Chromium in the image and set the executable path explicitly.
Process exits immediately in a container Sandbox, shared-memory, permissions, or missing system libraries Use a supported container setup, grant the runtime user access, review stderr, and apply only the isolation flags your platform requires.
Fonts or images are missing Network restrictions, certificate errors, relative URLs, or fonts not installed Use absolute URLs, install fonts and CA certificates, and wait for image/font readiness.
Modern CSS differs from an old deployment wkhtmltopdf’s Qt WebKit does not match current Chrome Move the workload to Chrome or test every required layout feature before retaining wkhtmltopdf.
PDF has unwanted date, URL, or page numbers Chrome print header/footer decoration is enabled Use the no-header-footer option in your Chrome or Puppeteer integration.
Works locally but times out in production Different DNS, CPU, memory, browser version, or network timing Run the same URL and readiness policy in the production image, log timings, and set a bounded job timeout.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website capture API and MCP server when you would rather send a URL than maintain a browser worker. It removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the page verdict and billing result in headers. Its API can return PNG, JPEG, WebP, or PDF and supports waits, full-page capture, custom CSS and JavaScript, authentication headers and cookies, PDF paper settings, signed webhooks, and bulk requests.

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

PHP can call the same endpoint with the standard HTTP client:

<?php
$url = 'https://api.screenshotneo.com/v1/shot';
$query = http_build_query([
    'access_key' => 'YOUR_API_KEY',
    'url' => 'https://stripe.com',
]);
$data = file_get_contents($url . '?' . $query);
file_put_contents(__DIR__ . '/shot.webp', $data);

For the complete option list and PDF settings, see the ScreenshotNeo documentation. The service also exposes an MCP server with take_screenshot, get_page_info, and capture_pdf tools, so Claude, Cursor, or another MCP client can perform captures.

Python and Node.js clients can use the same one-request API:

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}`);

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Sign up for the free ScreenshotNeo plan.

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

Which method should you use?

  • Choose Browsershot when your PHP or Laravel application needs a practical API for URLs or raw HTML, readiness waits, and print options.
  • Choose chrome-php/chrome when direct PHP control and explicit browser lifecycle management matter.
  • Choose the Chrome CLI for a small, process-oriented worker with simple waiting requirements.
  • Choose Dompdf only when the document is static and does not require JavaScript.
  • Keep wkhtmltopdf only after validating its older engine against your actual pages.

FAQ

Can JavaScript that uses browser storage run during PDF generation?

Yes, provided the renderer is launched with the required cookies, local-storage state, or authentication. A fresh browser context does not automatically contain the state from your interactive desktop session.

Why does a PDF look different from a screenshot?

PDF printing uses page size, margins, pagination, and print CSS. A viewport screenshot has no page breaks. Test both outputs if your workflow needs each.

Should I render PDFs inside a web request?

Only for short, predictable pages. For large reports or slow applications, queue a job, impose a hard timeout, and return a job status so one browser failure does not tie up a web worker.

Frequently Asked Questions

Can JavaScript that uses browser storage run during PDF generation?

Yes, provided the renderer is launched with the required cookies, local-storage state, or authentication. A fresh browser context does not automatically contain the state from your interactive desktop session.

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

Why does a PDF look different from a screenshot?

PDF printing uses page size, margins, pagination, and print CSS. A viewport screenshot has no page breaks. Test both outputs if your workflow needs each.

Should I render PDFs inside a web request?

Only for short, predictable pages. For large reports or slow applications, queue a job, impose a hard timeout, and return a job status so one browser failure does not tie up a web worker.

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.