Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

garaekz's avatar

Canio, a different approach to Laravel PDF rendering: readiness contract instead of timing heuristics

I want to share an approach more than promote a project, because I'm curious what this community thinks of the tradeoff.

The problem: every HTML-to-PDF setup I've shipped on headless Chrome has the same failure mode. The renderer decides when to capture (network idle + an arbitrary delay), but only the application knows when the document is genuinely done, webfonts swapped in, Chart.js finished animating, async data painted. So you end up with waitUntilNetworkIdle() + delay(2000), it works locally, and then it flakes on a queue worker in production.

The approach I took in Canio: invert the control. The page declares readiness explicitly:

window.__CANIO_READY__ = true;

You set it whenever "done" is actually true for that document, after document.fonts.ready, after the chart's onComplete, after the fetch resolves, whatever applies. The runtime blocks on that signal with a timeout fallback. Capture becomes deterministic with respect to your app instead of the network.

The second piece, which I find more interesting than the readiness contract itself: every render can persist an artifact bundle, the exact HTML sent to Chrome, a DOM snapshot at capture time, a screenshot, the console log, and the network log. When someone reports "the invoice from three days ago looked wrong," you open the screenshot artifact and see exactly what Chrome saw, instead of trying to reproduce a nondeterministic render.

Under the hood it's a Go runtime over CDP (no Node dependency in the deploy), it ships its own pinned Chrome for Testing, MIT licensed, Laravel 10–13.

Repo: oxhq/canio (can't post links)

I still use and recommend Browsershot for simple static documents, this isn't a replacement pitch. Canio is for the cases where capture timing or production debuggability genuinely bite you.

Genuinely interested in pushback on the readiness-contract model. Is explicit readiness the right call, or do you prefer keeping the heuristic and tuning it? What breaks for you with HTML-to-PDF?

1 like
2 replies
imrandevbd's avatar

I've been dealing with PDF generation in Laravel for years, and the waitUntilNetworkIdle() + arbitrary sleep() combo is a universal ticking time bomb. It always works perfectly locally and randomly flakes on production queues when a single webfont or third-party script takes 200ms too long to load.

Inverting the control to window.CANIO_READY is 100% the right call for complex documents (Chart.js, async data). It turns a race condition into a deterministic state. I've had to hack together similar window flags in raw Puppeteer scripts in the past just to stop the bleeding, so having it natively handled by a package is a massive win.

1 like
garaekz's avatar

Hey, thanks, that’s exactly what I was aiming for, prevent custom approaches and package it into a single deterministic place

Please or to participate in this conversation.