Load JavaScript from a String for HTML-to-PDF in PHP
Learn how to execute JavaScript from an HTML string and create reliable PDFs in PHP with headless Chrome, hosted APIs, and practical fixes.

Direct answer: PHP itself does not execute JavaScript when it converts an HTML string to PDF. If the rendered document depends on JavaScript, use a real browser engine such as headless Chrome, wait for the page’s data and assets to finish loading, then print the page to PDF. Libraries such as Dompdf and mPDF can accept HTML strings, but they are layout engines rather than full browser runtimes. Dompdf’s documentation explicitly says that it does not run JavaScript, while mPDF recommends headless Chrome for modern CSS and page mirroring.
This guide shows a self-hosted PHP implementation with chrome-php/chrome, explains how to make scripts and assets resolvable, covers waiting strategies, and compares browser, PHP-only, command-line, and hosted approaches. It also includes security, reliability, performance, cost, troubleshooting, and a hosted option when operating Chromium is not practical.
1. Choose a renderer that executes JavaScript
The first decision determines whether your charts, client-side templates, API-loaded data, and DOM mutations will appear in the PDF.
| Option | Runs JavaScript? | Best fit | Main trade-off |
|---|---|---|---|
| Headless Chrome | Yes | Modern applications, charts, dynamic data, accurate print CSS | Requires a Chrome/Chromium binary and browser process management |
| Dompdf | No | Controlled, mostly static HTML/CSS | DOM changes and JavaScript-generated content are absent |
| mPDF | No | Static UTF-8 documents, tables, headers, footers, barcodes, and table of contents | It is a dated layout engine and is not a general browser |
| wkhtmltopdf | Some, through Qt WebKit | Existing workflows already validated with its engine | Modern browser APIs and asynchronous applications require page-specific testing |
| Hosted Chrome API | Yes | Teams that cannot install or operate Chromium | External service dependency and per-use cost |
Google’s headless Chrome documentation describes executing page scripts before serializing the DOM and supports printing with --print-to-pdf. For a PHP application, a browser-control library is usually easier to integrate than invoking Chrome manually.
2. Self-hosted PHP with chrome-php/chrome
chrome-php/chrome controls Chrome or Chromium from PHP, evaluates JavaScript, waits for page events, and creates PDFs. Its documented requirements include PHP 7.4–8.5 and Chrome or Chromium 65 or newer. Install it with Composer:

composer require chrome-php/chrome
Make sure the server can launch the browser. In containers, this commonly means installing Chromium and supplying a writable temporary directory. Run the browser with an appropriate sandbox configuration for your deployment rather than copying flags blindly from an unrelated environment.
Complete example: HTML string, script execution, and PDF
This example writes the HTML string to a temporary local file, opens it in Chrome, waits for an application-defined marker, and saves a PDF. A file URL is used because it gives the browser a concrete base location for relative assets. For user-controlled HTML, use an isolated rendering environment and apply the security controls described later.
<?php
require __DIR__ . '/vendor/autoload.php';
use HeadlessChromium\BrowserFactory;
$html = <<<'HTML'
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>JavaScript report</title>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: Arial, sans-serif; color: #222; }
.chart { height: 180px; border: 1px solid #ccd3dc; padding: 16px; }
</style>
</head>
<body>
<h1>Monthly report</h1>
<div id="chart" class="chart">Preparing chart…</div>
<div id="ready" hidden>ready</div>
<script>
const values = [12, 28, 19, 36];
const chart = document.querySelector('#chart');
chart.textContent = `Values: ${values.join(', ')}`;
document.querySelector('#ready').hidden = false;
</script>
</body>
</html>
HTML;
$input = tempnam(sys_get_temp_dir(), 'pdf-html-') . '.html';
$output = __DIR__ . '/report.pdf';
file_put_contents($input, $html);
$browserFactory = new BrowserFactory();
$browser = $browserFactory->createBrowser([
'headless' => true,
// Set this when Chrome is not on PATH:
// 'executablePath' => '/usr/bin/chromium',
]);
try {
$page = $browser->createPage();
$page->navigate('file://' . $input)->waitForNavigation();
// Wait for the application state, not an arbitrary short sleep.
$page->waitForFunction(
"document.querySelector('#ready') && !document.querySelector('#ready').hidden",
10000
);
$page->pdf([
'printBackground' => true,
'preferCSSPageSize' => true,
])->saveToFile($output);
} finally {
$browser->close();
@unlink($input);
}
echo "Created {$output}\n";
The exact method names can vary between library versions. Check the version’s API when upgrading, and validate the browser executable and PDF options in your deployment.
Use a served URL when assets need HTTP
If the HTML references relative stylesheets, images, fonts, modules, or API endpoints, serve it from an application route or a temporary local web server. Then navigate to an https:// or http:// URL. A URL gives the browser an ordinary origin and makes relative paths predictable. If you generate the document from a string, include a <base href="https://your-app.example/"> element or convert relative asset paths to absolute URLs.
3. Load JavaScript from the string correctly
Inline scripts
Inline <script> blocks execute when Chrome parses them. Put them after the elements they modify, or wait for DOMContentLoaded. If a script depends on a library, load that library first and verify that its URL is reachable from the rendering machine.
External scripts
External scripts require a resolvable URL, DNS access, TLS trust, and a compatible content type. A browser may also block requests because of CSP, CORS, authentication, or mixed content. Capture console and network errors during development and make your application expose a deterministic ready marker after all required work is complete.
Asynchronous data
Do not print immediately after navigation when a script performs fetch(), imports modules, waits for a chart animation, or loads web fonts. Prefer one of these signals:
- An element such as
#pdf-readybecomes visible after rendering finishes. - A JavaScript promise resolves after data, images, and fonts are ready.
- A documented network-idle condition is sufficient for pages without long polling or analytics requests.
window.renderComplete = Promise.all([
document.fonts.ready,
fetch('/api/report').then(r => r.json()).then(data => drawReport(data))
]).then(() => {
document.body.dataset.pdfReady = 'true';
});
Then wait for document.body.dataset.pdfReady === 'true' from PHP. A fixed sleep can work as a temporary diagnostic, but it is fragile: slow fonts or an overloaded API can make the PDF incomplete.
4. Print settings that affect the PDF
- Backgrounds: enable background printing when colored panels, charts, or images are part of the design.
- Paper size and margins: use CSS
@pagerules or the library’s paper options. Avoid conflicting settings unless you know which one wins. - Print media: define
@media printrules for navigation, interactive controls, and page breaks. - Page breaks: use
break-before,break-after, andbreak-inside; test long tables and cards. - Fonts: wait for
document.fonts.readyand ensure the renderer can reach every font URL. - Images: use absolute URLs or data URLs when appropriate, and wait for image loading before printing.
- Animations: disable transitions and animations in print CSS so the captured frame is deterministic.
@media print {
.no-print, nav, button { display: none !important; }
* { animation: none !important; transition: none !important; }
.section { break-inside: avoid; }
}
5. PHP-only alternatives and when they fit
Dompdf can render an HTML string with loadHtml(), but its official tutorial says it does not run JavaScript. Use it when the final document is already present in the HTML and requires CSS support within its documented limits.
$dompdf->loadHtml($html);
$dompdf->setPaper('A4');
$dompdf->render();
file_put_contents('report.pdf', $dompdf->output());
mPDF’s WriteHTML() also accepts an HTML string and is useful for controlled templates, pagination features, headers, footers, barcodes, and tables of contents. Its manual describes the software as dated and recommends headless Chrome for state-of-the-art CSS or mirroring existing HTML pages. Neither library should be selected with the expectation that arbitrary browser JavaScript will run.
wkhtmltopdf uses the Qt WebKit engine and can be called from PHP after writing the string to its input. Treat it as an engine choice that needs validation against your application’s JavaScript, browser APIs, and asynchronous behavior.
6. Security for untrusted HTML and JavaScript
An HTML string may contain scripts, event handlers, external requests, and URLs that expose server resources. Sanitize and constrain it before rendering.
- Use an allowlist for HTML elements, attributes, CSS, and URL schemes.
- Never expose cloud credentials, environment variables, filesystem secrets, or internal metadata endpoints to page scripts.
- Restrict outbound network access where possible and deny private IP ranges if external URLs are accepted.
- Control local-file access and do not let untrusted markup navigate to arbitrary
file://paths. - Run Chromium with a dedicated low-privilege user, resource limits, and an isolated temporary directory.
- Set timeouts and terminate stuck browser processes.
- Remember that mPDF’s manual warns that it is not intended to receive outside HTML/CSS without stronger validation than ordinary browser sanitization. Apply the same discipline to browser-based rendering.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| JavaScript-generated content is missing | A PHP-only renderer was used, or printing happened before the script ran | Use headless Chrome and wait for a ready marker |
| PDF is blank | Navigation failed, the page is still loading, or the HTML was malformed | Log the final HTML, navigate to a served URL, inspect console errors, and wait for readiness |
| Charts are incomplete | Data fetch, canvas drawing, or fonts had not finished | Wait for the chart’s completion promise and document.fonts.ready |
| Images or CSS are missing | Relative URLs have no useful base, or the browser cannot reach the host | Use absolute URLs or <base>, verify DNS/TLS/authentication, and wait for image loads |
| Navigation timeout | Long polling, blocked third-party requests, or a slow application | Use an application marker, block unnecessary requests, and set a bounded timeout |
| Chrome will not start | Missing binary, incompatible version, permissions, sandbox, or unwritable temp directory | Verify the executable path, PHP user permissions, browser version, and container configuration |
| Fonts differ from the web page | Font URL is unreachable or capture occurs before fonts load | Bundle or serve fonts reliably and wait for document.fonts.ready |
| Pages break in the wrong places | Screen CSS is being used for print, or content cannot avoid a break | Add @media print, @page, and break rules; test long content |
8. Performance, reliability, and cost
There is no universal speed or memory benchmark for these approaches. Runtime depends on HTML size, JavaScript, network assets, fonts, browser version, and server limits. Measure representative documents in your own environment.
- Reuse browser processes carefully: starting Chrome for every request adds startup latency, while a long-lived process needs isolation and crash recovery.
- Reduce page work: disable animations, defer nonessential widgets, and avoid loading analytics and advertisements in the print route.
- Cache stable inputs: cache PDFs or rendered data when the source and parameters are unchanged.
- Bound resource use: apply navigation, script, memory, and output-size limits; clean temporary files in finally blocks.
- Make retries safe: use idempotent document identifiers and avoid duplicating side effects in page scripts.
- Observe the pipeline: record render duration, browser exit status, page errors, timeout reason, and output size without logging secrets.
Self-hosting shifts cost to browser infrastructure, memory, maintenance, and operational time. A hosted browser service shifts that work to an external provider. Compare the total cost for your traffic, security requirements, and document complexity rather than assuming a PHP-only library is cheaper.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot and PDF API. It runs the browser capture for you and supports PDF output, including paper size, margins, landscape mode, and page ranges. Use the API when your source is a reachable URL instead of an in-memory HTML string; publish the generated document behind an authenticated or temporary URL first.

One request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/report -o report.pdf
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/report"},
timeout=90,
)
r.raise_for_status()
open("report.pdf", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const pdf = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('report.pdf', pdf));
See the ScreenshotNeo API documentation for PDF parameters and response details. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and try the PDF capture with no card.
10. Practical decision checklist
- Does the final document depend on JavaScript? If yes, use a real browser.
- Can every script, stylesheet, image, and font resolve from the renderer?
- What exact DOM marker or promise means the page is ready?
- Are print CSS, page size, margins, backgrounds, and page breaks defined?
- Is the HTML trusted, sanitized, and isolated from credentials and private services?
- Are browser startup, timeout, retry, cleanup, and crash-recovery paths implemented?
- Would a hosted browser API reduce operational work for your traffic and security model?
FAQ
Can I execute JavaScript with Dompdf?
No. Dompdf can render an HTML string, but it does not provide a browser JavaScript runtime.
Should I use mPDF for a React or Vue page?
Usually no. Those pages need a browser to execute their client-side code. Use headless Chrome or a hosted browser service.
Is a five-second sleep enough?
It may pass on a fast machine and fail under load. A deterministic DOM marker, promise, or appropriate network-idle condition is safer.
Can a browser PDF include authenticated data?
Yes, if the rendering context receives the required authentication safely. Prefer short-lived credentials, an isolated renderer, and a route designed for server-side capture.
Can ScreenshotNeo accept an HTML string directly?
The ScreenshotNeo call uses a URL. Host the generated HTML at a controlled URL, then request that URL for capture.


