wkhtmltopdf has been the default answer for "render this Rails view as a PDF" for over a decade. It's also been quietly falling behind for most of that time.
wkhtmltopdf wraps a specific, frozen snapshot of WebKit — Qt WebKit, not the WebKit that ships in current Safari — compiled into a standalone binary your app shells out to. That snapshot predates widespread flexbox support, has no CSS Grid, and its font/subpixel rendering reflects whatever WebKit looked like when that build was cut. The upstream project has had no meaningful release in years. None of this is a bug you can file — it's the nature of shipping a frozen rendering engine as a binary dependency.
Practically, this shows up as: a flexbox layout that renders fine in every browser your users
actually use, but collapses or misaligns in the PDF; a CSS Grid layout that doesn't render at
all; a custom web font that falls back silently; print-specific CSS (@page,
break-inside) that wkhtmltopdf only partially honors. The workaround is almost
always the same: maintain a second, simplified stylesheet just for the PDF path, duplicating
layout logic you already wrote once for the browser.
Headless Chrome is not a specialized "PDF rendering mode" — it's the same Chromium rendering
engine your users already browse the app with, running without a visible window, driven over
the Chrome DevTools Protocol (CDP). Page.printToPDF is a real, maintained CDP
command Google ships and tests as part of Chrome itself, not a third-party reimplementation of
"what a browser does." Flexbox, Grid, custom fonts, and print-specific CSS all render the way
they actually look in a browser, because the thing generating the PDF is a browser.
This is also why the Rails integration details matter more than they would for a simpler wkhtmltopdf shell-out: CDP gives you a real programmatic surface — navigate, wait for load, print — instead of "invoke a binary and hope," which is what makes a pooled, reusable browser process (see BrowserPool) both possible and worth doing.
The dependency gets heavier, not lighter. wkhtmltopdf is a single static
binary; Chrome/Chromium is a full browser install — tens to a few hundred megabytes depending
on the base image, more moving parts (shared libraries, fonts, sandboxing), and in a Docker
image running as root, an explicit --no-sandbox flag Chrome otherwise refuses to
start without. None of this is exotic — it's the same Chrome install your end-to-end test suite
probably already needs if you use Capybara/Selenium/Cuprite — but it is a real operational line
item a wkhtmltopdf binary never was. See
Chrome in Docker
for the actual install step.
A page costs more to render. Launching Chrome takes on the order of hundreds of milliseconds — negligible once, expensive per request. A pooled, long-lived browser process (one per Ruby process, not one per request) amortizes that cost away for the common case, at the cost of a shared resource every thread in that process now has: a single pathological page can tie it up for everyone until Chrome's own timeout fires.
If your PDF output is simple — plain text, a basic table, no flexbox/Grid, no custom web fonts — wkhtmltopdf's rendering gap may never actually bite you, and a single static binary is genuinely simpler to deploy than a browser. The switch matters most exactly when your PDF output needs to look like a modern web page, because that's precisely the case a decade-old frozen WebKit build was never going to handle well.
around_action, not a
before_action.