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.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →#1 Best Overall
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.
Recommended Free Tools
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.
Rank #2
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:
- Selector wait: have the application add a marker such as
#report-readyonly 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.
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.
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.
Rank #4
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. |
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.
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.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsWhich 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.
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.
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.

