Blog

Langsame PDF-Generierung in Cloudflare Workers? Diagnose in 5 Minuten

Workers sind schnell, bis Sie ihnen einen PDF-Stack geben, der für langlebige Server gebaut wurde. Die echten Bottlenecks, und wie Sie sie umgehen, wenn Sie PDFs am Edge brauchen.

Sie haben Ihren Service für Rechnungen, Labels oder Belege nach Cloudflare Workers verschoben, weil der Rest Ihres Stacks dort bereits läuft und die Latenzrechnung großartig aussieht: 5 ms zum nächsten Colo, 1 ms CPU, Request erledigt.

Dann landet PDF-Generierung bei Ihnen, und plötzlich sehen Sie 800 ms p99, Warnungen vor 50 MB großen Worker-Bundles und das hartnäckige Gefühl, dass Sie das falsche Werkzeug verwenden. Hier ist, warum das passiert und welche Bottlenecks Sie an einem Nachmittag beheben können.

Workers sind nicht Lambda, und das zählt

Vor der Diagnose muss das Laufzeitmodell stimmen. Cloudflare Workers sind KEINE serverlosen Container. Sie sind V8 isolates mit diesen Grenzen:

  • CPU-Zeitlimit: 50 ms pro Request im Free-Plan, 30 Sekunden bei Workers Paid (Bundled), 5 Minuten bei Unbound. Wall Time ist unbegrenzt, aber abrechenbar.
  • Speicherlimit: 128 MB pro isolate.
  • Bundle-Größe: 1 MB im Free-Plan, 10 MB im Paid-Plan.
  • Kein Dateisystem. Kein fs.readFileSync. Alles liegt im Speicher oder wird geholt.
  • Keine nativen Binaries. Nur reines JavaScript / WebAssembly: kein node-canvas, keine nativen zlib-Aufrufe, kein Shell-Out zu Ghostscript.
  • Cold Start: etwa 5 ms. Erstaunlich schnell, aber nur, weil nichts Großes bootet.

Die meisten “PDF ist langsam in Workers”-Probleme entstehen, weil eine dieser Grenzen verletzt wird, meistens CPU-Limit oder Bundle-Größe, und das dann still drosselt.

Die fünf Dinge, die wirklich langsam sind

In ungefähr der Reihenfolge, in der sie Teams treffen:

1. Chromium-basierte Renderer in Workers ziehen

Das funktioniert schlicht nicht. Puppeteer braucht etwa 250 MB Chromium und ein echtes Betriebssystem. Browser-Rendering-Dienste wie Cloudflares eigene Browser Rendering API oder Browserless funktionieren, aber sie sind keine Workers. Sie sind ein separater Service, den Sie AUS einem Worker heraus aufrufen, mit etwa 500 ms Round-trip plus Renderzeit.

Wenn Ihr “Worker-basiertes PDF” eigentlich “Worker ruft eine entfernte Browser-Rendering-API auf” bedeutet, liegt Ihre Latenzuntergrenze bei etwa 500 ms. Das ist kein Worker-Problem; das ist die Browser-Steuer, die Ihnen bis an den Edge folgt.

Diagnose: Prüfen Sie, ob Ihr Code fetch("https://browser-rendering.cloudflare.com/...") oder etwas Ähnliches ausführt. Wenn ja, messen Sie die Latenz des Upstream-Services, nicht die des Workers.

2. Layout in JavaScript ausführen

Wenn Sie eine eigene JS-basierte Layout-Engine geschrieben haben - “ich berechne die Boxpositionen eben selbst” -, laufen Sie in das CPU-Limit. JavaScript ist schnell, aber Layout für mehr als 30 Elemente mit Textumbruch überschreitet auf Workers Free leicht 50 ms und liegt auf Bundled schnell bei 100 bis 300 ms.

Eine Render-Pipeline in dieser Form:

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

…macht vier CPU-gebundene Durchläufe über dieselben Daten. Jeder in JS, jeder mit Garbage-Collection-Druck, jeder mit neu allokierten Zwischenbäumen.

Diagnose: Suchen Sie in wrangler tail nach einem Render. Wenn vor jedem I/O mehr als 50 ms CPU anfallen, ist es ein Compute-Problem.

3. Fonts bei jedem Request laden

Fonts sind jeweils 50 bis 250 KB groß. Wenn Ihr Renderer sie bei jedem Render aus KV oder R2 liest, ist das ein Network Round-trip pro Font und pro Request. Fünf Fonts bedeuten fünf RTTs, also 50 bis 150 ms, bevor das Rendering beginnt.

Diagnose: Fügen Sie Timing zu Ihrem Font-Loading-Code hinzu. Wenn es p50 dominiert, ist das der Engpass.

Fix: Laden Sie Fonts einmal zur Modulinitialisierung, also oben in Ihrer Worker-Datei und NICHT im Request-Handler. Das isolate cached die Bytes für seine Lebensdauer, typischerweise Minuten bis Stunden.

// 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 });
  }
};

Wenn Ihr Bundler den Font als Bytes inlined, umso besser: gar kein I/O.

4. Eine JS-PDF-Bibliothek verwenden, die nicht für Workers gebaut wurde

pdfkit, pdf-lib, jsPDF: Alle funktionieren in Workers, aber ihre Eigenschaften tun weh:

  • pdfkit benötigt Node-Buffer-Shims. Möglich, aber das fügt etwa 500 KB hinzu und verlangsamt Compute um rund 30 %.
  • pdf-lib ist sehr gut beim Bearbeiten bestehender PDFs, weniger gut beim Erzeugen von Grund auf. Die Abstraktion fügt ungefähr 10 ms Overhead pro Seite hinzu.
  • jsPDF ist browser-first; gleiches Buffer-Problem, plus eine große API-Oberfläche, die schwer zu tree-shaken ist.

Für eine Render-Pipeline, die hauptsächlich “JSON lesen, PDF-Bytes schreiben” heißt, ist eine zweckgebaute Engine, die PDF direkt ausgibt und nicht über eine generische PDF-Abstraktion geht, 5- bis 20-mal schneller. WebAssembly-Engines aus Rust oder C++ profitieren zusätzlich von JIT-freundlichen engen Loops.

5. Das Bundle ist heimlich 4 MB groß

Workers Free begrenzt das Bundle auf 1 MB, Workers Bundled auf 10 MB. Viele Teams entdecken das Limit erst, wenn wrangler deploy mit “Script exceeds size limit” scheitert. Manche merken es früher, wenn ein riesiger Import den Cold Start verlangsamt, weil V8 alles kompilieren muss.

wrangler zeigt die Bundle-Größe im Deploy-Output. Alles über 500 KB verdient Prüfung. Häufige Ursachen:

  • Gebündelte Fonts. Verschieben Sie sie zu Workers Assets und fetchen Sie sie einmal zur Modulinitialisierung.
  • Die node:-Shim-Schicht. Wenn Sie __cf_KV oder polyfills: in der Source Map sehen, shimmed Ihr Bundler Node-APIs, die Sie wahrscheinlich nicht brauchen.
  • Ungenutzte Abhängigkeiten. npm run build -- --analyze liefert bei Wrangler 4+ eine Treemap.

Wie “schnelles PDF in Workers” wirklich aussieht

Ein zweckgebauter Edge-Renderer für strukturierte Dokumente - gPdf ist ein Beispiel, aber die Architektur gilt für jeden gut gebauten Renderer - sieht typischerweise so aus:

Metrik Typisch Warum
Cold Start 5 bis 20 ms V8 isolate boot + erster Load des WASM-Moduls
CPU pro Render 1 bis 4 ms enger WASM-Loop, kein GC-Druck
Wall Time pro Render 3 bis 8 ms CPU plus wenige Mikrosekunden Krypto für PDF-Objekt-IDs
Bundle-Größe 4 bis 6 MB Renderer plus gebündelte Fonts (Lateinisch + CJK NotoSans)
Peak-Speicher 8 bis 20 MB Dokumentbaum plus ausgegebener PDF-Puffer

Vergleichen Sie das mit dem typischen Pfad “Puppeteer-on-Workers über entfernten Browser-Renderer”: 500 bis 1.000 ms p50, 1 bis 2 GB Browser-Speicher irgendwo anders gehostet und ungefähr 0,001 USD Upstream-Kosten pro Render.

Schnelle Triage

Wenn Sie gerade unter langsamen PDFs in Workers leiden, gehen Sie diese Checkliste zuerst durch:

  1. Wohin geht die Zeit? Setzen Sie Timestamps in wrangler tail. Klären Sie, ob der Engpass CPU, Egress-fetch oder Cold Start ist.
  2. Führen Sie JS-Layout aus? Wenn ja, ist das wahrscheinlich der Großteil Ihrer CPU. Wechseln Sie zu einem Renderer, der Layout vorkompiliert.
  3. Laden Sie Fonts pro Request? Verschieben Sie Font-Loading in die Modulinitialisierung.
  4. Rufen Sie einen externen Browser auf? Dann ist Ihre Latenzuntergrenze die Antwortzeit dieses Services. Wechseln Sie zu einem Renderer im selben isolate, ohne fetch.
  5. Ist Ihr Bundle über 1 MB? Cold Start skaliert mit Bundle-Größe. Entfernen Sie ungenutzte Dependencies.

Das schnellstmögliche PDF in Workers ist eines, bei dem Dokumentdaten zu ausgegebenen PDF-Bytes vollständig innerhalb eines einzigen fetch-Handler-Aufrufs werden, ohne eigene fetch()-Aufrufe und ohne CPU-lastiges JS-Layout. Die meisten Beschwerden “Workers sind langsam für PDF”, die wir hören, bedeuten eigentlich: “Wir haben einen nicht-Worker-förmigen PDF-Stack in Workers gelegt und die Nachteile beider Welten bekommen.”

Kurzfassung

Cloudflare Workers können PDFs in einstelligen Millisekunden rendern, aber nur, wenn der Renderer für eine isolate-förmige Laufzeit gebaut ist. JS-PDF-Bibliotheken für Node, Browser-Rendering-Services aus einem Worker heraus, Fonts pro Request, Layout-Pässe in JavaScript: Jeder dieser Punkte drückt p50 irgendwo zwischen “langsamer als Ihr Origin” und “nicht von einer regionalen Lambda zu unterscheiden”.

Wenn Sie das nicht selbst herausentwickeln möchten, ist der gPdf Playground ein vollständig am Edge deployter Renderer auf genau dieser Runtime. Klicken Sie Render PDF und schauen Sie in den Network Tab: Dieser p50 zeigt, was ein Worker kann, wenn nichts in der Pipeline gegen ihn arbeitet.