Lifecycle events
PDF Server waits for a page lifecycle event before printing to PDF. By
default it waits for load — all synchronous resources are loaded and rendered.
For pages with async content (charts, fetch, web fonts), you can wait longer.
Choosing the event
Set it per template in params.json or globally with
--wait-lifecycle-event / WAIT_LIFECYCLE_EVENT:
{ "pdf": { "waitLifecycleEvent": "networkIdle" } }
Available events
| Event | Fires when… |
|---|---|
init | navigation begins |
DOMContentLoaded | HTML parsed, before subresources |
load | default — all sync resources loaded |
firstPaint / firstContentfulPaint | first pixels painted |
firstMeaningfulPaint / firstMeaningfulPaintCandidate | main content painted |
networkAlmostIdle | ≤2 network connections for a period |
networkIdle | no network activity for a period |
callback | the page explicitly signals readiness (see below) |
A web font is the most common reason to need one of the later events:
load does not wait for @font-face files, so a page can print in the
fallback font before its own arrives. See
Assets.
The callback event — signal readiness yourself
For fully custom "ready" logic (e.g. after a chart library finishes drawing),
use callback. The renderer then waits until the page calls back to the
internal server, rather than watching browser events.
{ "pdf": { "waitLifecycleEvent": "callback" } }
In the page, fetch the callback URL when your content is ready:
<script>
// ...draw charts / load data...
window.addEventListener('load', () => {
fetch(window.location.pathname + '/callback');
});
</script>
The renderer prints the PDF as soon as that request arrives (bounded by the
render timeout). See the callback example in the Gallery.
The endpoint accepts GET and POST, and ignores the request body — the
arrival of the request is the whole signal. POST is there for
navigator.sendBeacon, which can only send that verb and is the one way to
signal from a page that is being torn down; fetch and XMLHttpRequest send a
GET and are what the snippet above uses.
The callback endpoint lives on the loopback-only internal server the browser is already talking to — the page reaches it via its own
window.location. See Internal callback server.