Use a browser-based renderer such as Grover with Puppeteer and Chromium. Put the remote dependency in a normal <script src="https://…"> tag (or inject it with the browser’s script-tag API), wait until the JavaScript has finished changing the page, and only then call to_pdf. PDFKit and Wicked PDF can work for simpler pages, but they wrap wkhtmltopdf, so JavaScript behavior depends on the wkhtmltopdf build you deploy.
The reliable rendering sequence
A PDF library that only parses HTML cannot produce content created by JavaScript. The renderer must execute the page in a browser context, allow the remote script to load, wait for the application’s own ready condition, and then print the page.
- Build the HTML. Include the remote library with a complete HTTPS URL.
- Open it in Chromium. Grover delegates this work to Puppeteer.
- Wait for readiness. Prefer a selector or application flag over an arbitrary sleep.
- Print to PDF. Call
to_pdfafter the dynamic content is present.
Grover documents URL input, script-tag options and several waiting hooks; check the README for the exact option names supported by the version installed in your application: Grover README.
Put the remote JavaScript in the HTML
Normal script loading
Use a regular script element when the page’s own code should import and use the library during normal page loading:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.example.test/library.js"></script>
</head>
<body>
<div id="report"></div>
<script>
// Replace LibraryName with the global exposed by your dependency.
LibraryName.render({ target: '#report', value: 42 });
document.documentElement.dataset.pdfReady = 'true';
</script>
</body>
</html>
The readiness marker is application code, not a Grover requirement. It gives the renderer an unambiguous signal that the asynchronous work affecting the PDF is complete.
Injecting a script when you cannot edit the page
Puppeteer’s Page API supports adding a script tag by URL or by inline content. Grover exposes script-tag configuration and browser timing hooks, so you can add the dependency before conversion when the source HTML is fixed or comes from another endpoint. See the documented API: Puppeteer Page API.
Do not confuse Grover’s execute_script with an early dependency hook. The README describes it as supplementary JavaScript run after render and before conversion. That is suitable for a final DOM adjustment, but it is too late if page code needed the library while it was initializing. If initialization order matters, use a normal script element, a documented script-tag option, or an early-page mechanism such as evaluate_on_new_document; verify the exact option spelling in your installed Grover release.
A Ruby example with Grover
Install and render a local HTML document
Add Grover to your bundle and make sure its Puppeteer/Chromium prerequisites are installed according to the project documentation. This example writes an HTML string to a temporary file, opens it through Grover, waits for a page-level readiness selector, and writes the PDF bytes.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minute# Gemfile
gem 'grover'
require 'grover'
require 'tempfile'
html = <<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; }
@media print { .screen-only { display: none; } }
</style>
<script src="https://cdn.example.test/library.js"></script>
</head>
<body>
<h1>Monthly report</h1>
<div id="report">Loading…</div>
<script>
(async function () {
try {
await LibraryName.render({ target: '#report' });
document.querySelector('#report').dataset.ready = 'true';
document.documentElement.dataset.pdfReady = 'true';
} catch (error) {
document.documentElement.dataset.pdfError = error.message;
throw error;
}
}());
</script>
</body>
</html>
HTML
Tempfile.create(['report', '.html']) do |file|
file.write(html)
file.flush
# Grover's option names vary by release. Confirm the wait option in
# the README for the version in your Gemfile.lock.
pdf = Grover.new(
"file://#{file.path}",
options: {
format: 'A4',
wait_for_selector: '[data-ready="true"]'
}
).to_pdf
File.binwrite('report.pdf', pdf)
end
The Grover.new(url).to_pdf pattern is the stable core of the example. The selector-wait key shown above illustrates the required behavior, but Grover’s current option schema can change; use the README and the version locked in your bundle before deploying it. If your application serves the HTML over HTTP, pass that URL instead of a file:// URL.
Use an application-specific readiness signal
A selector such as [data-ready="true"] is stronger than sleep 2: it completes as soon as the work is done and fails visibly when the application never reaches the state. Grover also documents selector and function waits. Keep the timeout finite and log the page error marker so a failed API call cannot silently become a PDF containing “Loading…”.
Rank #2
When the dependency must run before page scripts
If inline application code calls the library immediately, loading it after that code will produce an undefined-global error. Put the remote script before the application script, or use the Puppeteer early-page/script-tag facility exposed by your Grover version. A post-render execute_script call cannot repair an initialization-order failure that already occurred.
Choosing a Ruby PDF engine
| Engine | JavaScript model | When it fits | Main caution |
|---|---|---|---|
| Grover + Puppeteer/Chromium | Runs page JavaScript in a browser before printing. | Dashboards, charts, client-rendered templates and pages whose output depends on asynchronous work. | Requires a Chromium-capable runtime and careful wait/readiness handling. Grover documentation |
| PDFKit | Wraps wkhtmltopdf. | HTML/CSS documents where the deployed wkhtmltopdf build handles the required scripts and assets. | Verify JavaScript support, external-resource access and the exact wkhtmltopdf binary. PDFKit README |
| Wicked PDF | Also wraps wkhtmltopdf. | Rails applications already standardized on its helpers and a compatible wkhtmltopdf installation. | Test the real production binary; browser behavior is not equivalent to Chromium. Wicked PDF README |
For a new JavaScript-heavy report, Grover is the most direct fit because its underlying browser has an explicit navigation, script and PDF API. Puppeteer’s PDF guide shows the browser-oriented flow: navigate, wait for the page state you need, then call page.pdf: Puppeteer PDF guide.
Make external URLs resolvable
Use absolute URLs or a configured base URL
A browser can resolve https://cdn.example.test/library.js without help. A relative URL such as /assets/report.js needs an HTTP origin or a configured base/root URL. PDFKit’s documentation specifically recommends complete paths in raw HTML and describes root_url and protocol settings: PDFKit README.
Check network access from the renderer
- Allow outbound DNS, TCP and TLS access from the worker or container.
- Follow redirects and confirm the final response is JavaScript, not an HTML login page.
- Supply authentication headers or cookies when the script host requires them.
- Check certificate chains and system time in minimal containers.
- Review Content Security Policy and other browser restrictions that can block the request.
Do not assume that a URL working in your laptop browser is reachable from a background worker, CI job or production container.
Avoid development-server deadlocks
PDFKit documents a failure mode in which a single-threaded development server renders a PDF and then waits for that same server to deliver CSS, images or JavaScript. Use embedded resources, a separate asset host, or a multi-worker server as appropriate. The same architectural concern applies whenever the renderer calls back into the process that is waiting for the render to finish.
Security and trust boundaries
Remote JavaScript is executable code. Pin a trusted host, use HTTPS, and consider integrity controls where your deployment supports them. Do not render arbitrary user-supplied pages in a privileged browser context. The Grover project includes the warning, “Do not enable if rendering content from outside entities (user uploads, external URLs, etc),” in the context of a particular option; read that warning alongside the option’s documentation rather than treating it as a blanket statement about every Grover setting: Grover README.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Rank #3
For untrusted input, isolate the browser process, restrict network egress, limit render time and memory, and avoid passing application credentials into the page. Treat third-party scripts as having the same authority as any other code executed by the rendering browser.
Printing details that affect the final PDF
Print media and CSS
Browser PDF generation uses print media by default. Add print-specific rules with @media print, hide navigation and controls, and use break-before, break-after or break-inside to control pagination. If colors or backgrounds are essential, verify the print-color behavior in the browser options used by your Grover version.
Fonts, images and charts
Wait for web fonts and images that affect layout. A chart library may finish drawing after the DOM selector appears, so set the readiness marker only after its final canvas or SVG is complete. For deterministic output, self-host or pin asset versions and avoid data that changes while the page is printing.
Time, locale and data consistency
Set the report’s timezone and locale explicitly in the page or browser configuration. Fetch all data before signaling readiness, and include a report timestamp so a retry can be distinguished from a new report. A PDF generated from a live dashboard can otherwise mix responses from different moments.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteTroubleshooting JavaScript PDF failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The PDF contains “Loading…” or an empty chart. | Conversion started before asynchronous rendering completed. | Set an application-specific readiness selector or function and wait for it; do not rely on a fixed short delay. |
LibraryName is not defined. |
The dependency was injected after page code ran, or the CDN request failed. | Load the script src before application code, use an early-page hook, and inspect browser network/console output. |
| The script works locally but not in production. | The worker cannot resolve DNS, reach the host, validate TLS or pass authentication. | Test connectivity from the renderer environment and provide the required headers or cookies. |
| CSS, images or JavaScript are missing. | Relative URLs have no base, resources are blocked, or the asset server is unavailable. | Use absolute URLs or configure a root URL; check CSP, redirects and server logs. |
| The render hangs while fetching assets from your app. | A single-threaded server is waiting on its own request. | Embed assets, serve them from another process/host, or run enough workers to handle nested requests. |
| A timeout occurs only for some reports. | Data-dependent requests, slow third-party assets or an unhandled rejected promise. | Capture console and network errors, set bounded request timeouts, fail the page explicitly, and wait for the real completion condition. |
| Fonts or layout shift between runs. | Fonts/images were still loading or external data changed during print. | Wait for those assets, pin versions, and freeze the report data before rendering. |
Operational and cost considerations
Chromium startup consumes more CPU and memory than a static PDF library. Reuse a controlled browser process where your deployment permits it, cap concurrent pages, and enforce navigation, JavaScript and overall job timeouts. Queue large batches instead of launching an unrestricted browser per request. Record the URL, renderer version, readiness outcome and failure reason so a missing PDF can be diagnosed without reproducing production data.
There is no universal performance number: execution time depends on page scripts, network latency, asset size and browser resources. Measure your own representative reports, including the slowest data path and a cold browser start. Cache immutable JavaScript and CSS at the network layer, but do not cache personalized report data unless its privacy and invalidation rules are explicit.
Rank #4
Or skip the browser setup
ScreenshotNeo provides a website capture API and MCP server for developers. A single request can return a PNG, JPEG, WebP or PDF; its PDF controls include paper size, margins, landscape mode and page ranges. The service can load lazy images, wait for a selector, delay or network idle, run custom JavaScript, set headers/cookies/user agents, and use a chosen viewport or device preset. It also removes cookie-consent banners, newsletter popups and chat widgets before capture when those cleanup steps are enabled.
For a quick URL capture, use the documented request pattern (replace the URL with your own):
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 complete options and PDF parameters in the ScreenshotNeo documentation. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed; the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.
FAQ
Can a remote script be loaded from a private URL?
Yes, if the browser can authenticate to that host. Pass credentials through the renderer’s supported headers or cookies, never by exposing secrets in a public HTML document, and verify that redirects do not send the request to an unintended host.
Should I use a fixed delay instead of a selector wait?
Use a selector or function tied to the application’s completed state whenever possible. A delay is only a fallback for a page with no reliable signal and must be long enough for the slowest supported environment.
Why does the PDF differ from the screen view?
Browser PDF output uses print media by default, so print CSS, page size, margins and pagination can intentionally change the result. Test the print stylesheet rather than comparing only the interactive screen.
Best Value
Is a browser renderer suitable for untrusted URLs?
Only with strong isolation and a deliberately restricted network and credential environment. Treat every fetched page and script as executable content and follow the renderer project’s security guidance.
Frequently Asked Questions
Can a remote script be loaded from a private URL?
Yes, if the browser can authenticate to that host. Pass credentials through supported headers or cookies, never by exposing secrets in public HTML, and verify redirects.
Should I use a fixed delay instead of a selector wait?
Prefer a selector or function tied to the application’s completed state. Use a delay only when no reliable readiness signal exists.
Why does the PDF differ from the screen view?
Browser PDF output uses print media by default, so print CSS, page size, margins and pagination can change the result.
Is a browser renderer suitable for untrusted URLs?
Only with strong process, network and credential isolation. Treat every fetched page and script as executable content.
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.

