Blog

Trage PDF-generatie in Cloudflare Workers? Diagnoseer het in 5 minuten

Workers zijn snel, totdat u ze een PDF-stack geeft die voor langlevende servers is ontworpen. De echte bottlenecks, en hoe u ze overslaat, wanneer u PDF's op de edge nodig hebt.

U hebt uw factuur-, verzendlabel- of bonservice naar Cloudflare Workers verplaatst omdat de rest van uw stack daar al draait, en de latency-rekensom er prachtig uitziet: 5 ms naar de dichtstbijzijnde colo, 1 ms CPU, request klaar.

Dan landt PDF-generatie op uw bord, en opeens kijkt u naar p99’s van 800 ms, waarschuwingen over worker bundles van 50 MB en het hardnekkige gevoel dat u de verkeerde tool gebruikt. Dit is waarom dat gebeurt, en welke echte bottlenecks u in een middag kunt oplossen.

Workers zijn geen Lambda, en dat maakt uit

Zet vóór de diagnose eerst het runtime-model goed. Cloudflare Workers zijn GEEN serverless containers. Het zijn V8 isolates met deze beperkingen:

  • CPU time limit: 50 ms per request op het Free-plan, 30 seconden op Workers Paid (Bundled), 5 minuten op Unbound. Wall time is onbegrensd maar factureerbaar.
  • Memory cap: 128 MB per isolate.
  • Bundle size: 1 MB voor het Free-plan, 10 MB op paid.
  • Geen filesystem. Geen fs.readFileSync. Alles staat in memory of wordt opgehaald.
  • Geen native binaries. Alleen pure JavaScript / WebAssembly; geen node-canvas, geen native zlib-calls, geen shell-out naar Ghostscript.
  • Cold start: ongeveer 5 ms. Uitzonderlijk snel, maar alleen omdat er niets groots hoeft te starten.

De meeste “trage PDF in Workers”-problemen ontstaan doordat een van die grenzen wordt overschreden, meestal de CPU-limiet of bundle size, waarna throttling stilletjes zichtbaar wordt.

De vijf dingen die werkelijk traag zijn

In grove volgorde van hoe vaak ze teams raken:

1. Chromium-gebaseerde render-engines in Workers trekken

Dit werkt niet, punt. Puppeteer heeft ongeveer 250 MB Chromium en een echt OS nodig. Browser-renderingservices, zoals Cloudflare’s eigen Browser Rendering API of Browserless, werken wel, maar zijn geen Workers: het zijn aparte services die u VANUIT een Worker aanroept, met ongeveer 500 ms roundtrip plus rendertijd.

Als uw “Worker-gebaseerde PDF” eigenlijk “Worker die een externe browser-rendering-API aanroept” betekent, is uw latencybodem ongeveer 500 ms. Dat is geen Worker-probleem; dat is de browserbelasting die u naar de edge volgt.

Diagnose: controleer of uw code fetch("https://browser-rendering.cloudflare.com/...") of iets vergelijkbaars doet. Zo ja, dan meet u de latency van de upstream service, niet die van de Worker.

2. Layout in JavaScript doen

Als u een eigen JS-gebaseerde layoutengine hebt geschreven (“ik bereken boxposities gewoon zelf”), raakt u de CPU-limiet. JavaScript is snel, maar layout voor meer dan 30 elementen met tekstwrapping gaat op Workers Free gemakkelijk over 50 ms, en op Bundled naar 100-300 ms.

Een renderpijplijn in deze vorm:

JSON → JS layout pass → SVG generation → SVG-to-PDF library → emit

doet vier CPU-bound passes over dezelfde data. Elk in JS, elk met garbage-collection overhead, elk met opnieuw gealloceerde tussenbomen.

Diagnose: zoek uw wrangler tail-log voor één render. Als u meer dan 50 ms CPU ziet vóór enige I/O, is dit een computeprobleem.

3. Fonts bij elke request laden

Fonts zijn 50-250 KB per stuk. Als uw renderlaag ze bij elke render uit KV / R2 leest, is dat één network roundtrip per font, per request. Vijf fonts betekent vijf RTT’s, dus 50-150 ms voordat renderen begint.

Diagnose: voeg timing toe rond uw font-loading code. Als die p50 domineert, is dit het probleem.

Fix: laad fonts één keer tijdens module-init, bovenaan uw Worker-bestand, NIET in de request handler. De isolate cachet de bytes gedurende de levensduur van de isolate (minuten tot uren).

// Module-init: runs ONCE per isolate
import notoSans from "./fonts/noto-sans.woff2";
const FONT_BYTES = new Uint8Array(notoSans);

export default {
  async fetch(request) {
    // Per-request: zero font I/O
    return renderPdf({ fontBytes: FONT_BYTES, ...data });
  }
};

Als uw bundler het font als bytes inline’t, is dat nog beter: helemaal geen I/O.

4. Een JS PDF-bibliotheek gebruiken die niet voor Workers is gebouwd

pdfkit, pdf-lib, jsPDF: ze werken allemaal in Workers, maar hebben eigenschappen die pijn doen:

  • pdfkit vereist Node Buffer shims. Dat kan, maar voegt ongeveer 500 KB toe en vertraagt compute met ongeveer 30%.
  • pdf-lib is sterk voor het bewerken van bestaande PDF’s, minder sterk voor vanaf nul uitgeven; de abstractielaag voegt ongeveer 10 ms overhead per pagina toe.
  • jsPDF is browser-first; hetzelfde Buffer-probleem, plus een breed API-oppervlak dat moeilijk te tree-shaken is.

Voor een renderpijplijn die vooral “lees JSON, schrijf PDF-bytes” doet, is een doelgebouwde engine die PDF direct uitgeeft, zonder generieke PDF-abstractie, 5-20× sneller. WebAssembly-engines die uit Rust of C++ zijn gecompileerd profiteren extra van de JIT-vriendelijke tight loop.

5. De bundle die stiekem 4 MB is

Workers Free beperkt de bundle tot 1 MB. Workers Bundled tot 10 MB. De meeste teams ontdekken de limiet wanneer wrangler deploy faalt met “Script exceeds size limit”. Sommige ontdekken het eerder, wanneer een enorme import cold start vertraagt doordat V8 alles moet compileren.

wrangler toont bundle size in de deploy-output. Alles boven 500 KB verdient onderzoek. Veelvoorkomende oorzaken:

  • Gebundelde fonts. Verplaats ze naar Workers Assets en fetch één keer tijdens module-init.
  • De node: shimlaag. Als u __cf_KV of polyfills: in de sourcemap ziet, shimt uw bundler Node-API’s die u waarschijnlijk niet nodig hebt.
  • Ongebruikte dependencies. npm run build -- --analyze (op Wrangler 4+) geeft een treemap.

Hoe snelle PDF in Workers eruitziet

Een doelgebouwde edge-renderlaag voor gestructureerde documenten, gPdf is één voorbeeld maar de architectuur geldt voor elke goed gebouwde variant, ziet er meestal zo uit:

Metriek Typisch Waarom
Cold-start 5-20 ms V8-isolate boot + eerste load van de WASM-module
CPU per render 1-4 ms WASM tight loop, geen GC-druk
Wall time per render 3-8 ms CPU + enkele µs crypto voor PDF object-ID’s
Bundle size 4-6 MB Render-engine + gebundelde fonts (Latin + CJK NotoSans)
Geheugenpiek 8-20 MB Documentboom + uitgegeven PDF-buffer

Vergelijk dat met het typische “Puppeteer-on-Workers via remote browser rendering”-pad: 500-1000 ms p50, 1-2 GB browsergeheugen elders gehost en ongeveer $0.001/render aan upstreamkosten.

Een snelle triage

Als u nu last hebt van trage PDF in Workers, doorloop dan eerst deze checklist:

  1. Waar gaat de tijd heen? Voeg timestamps toe in wrangler tail. Stel vast of de bottleneck zit in:
    • CPU (werk binnen het proces)
    • Egress fetch (aanroep naar een upstream service)
    • Cold start (alleen de eerste request na een rustige periode)
  2. Draait u JS-layout? Zo ja, dan is dat het grootste deel van uw CPU. Stap over op een engine die layout vooraf berekent.
  3. Laadt u fonts per request? Verplaats fontloading naar module-init.
  4. Roept u een externe browser aan? Dan is uw latencybodem de responstijd van die service. Stap over op een render-engine in dezelfde isolate, zonder fetch.
  5. Is uw bundle groter dan 1 MB? Cold start schaalt met bundle size. Snijd ongebruikte dependencies weg.

De snelst mogelijke PDF op Workers ontstaat wanneer uw documentdata binnen één fetch handler-call verandert in uitgegeven PDF-bytes, zonder eigen fetch()-calls en zonder CPU-zware JS-layout. De meeste klachten die wij horen als “Workers zijn traag voor PDF” betekenen eigenlijk: “we hebben een PDF-stack die niet bij Workers past op Workers gezet en kregen het slechtste van twee werelden.”

De korte versie

Cloudflare Workers kunnen PDF’s in enkelcijferige milliseconden renderen, maar alleen wanneer de render-engine voor een isolate-vormige runtime is gebouwd. JS-gebaseerde PDF-bibliotheken voor Node, browser-renderingservices die vanuit een Worker worden aangeroepen, fonts die per request worden geladen, layoutpasses in JavaScript: elk daarvan begrenst uw p50 ergens tussen “trager dan uw origin” en “niet te onderscheiden van een regionale Lambda”.

Als u dit liever niet zelf uitengineert, is de gPdf Playground een volledig op de edge gedeployde render-engine die op precies deze runtime draait. Klik Render PDF, kijk naar de Network-tab; die p50 is wat een Worker kan doen wanneer niets in de pijplijn tegen de omgeving vecht.