Blog

Wolne generowanie PDF w Cloudflare Workers? Zdiagnozuj je w 5 minut

Workers są szybkie — dopóki nie przeniesiesz do nich stosu PDF zaprojektowanego dla długowiecznych serwerów. Faktyczne wąskie gardła i sposoby ich ominięcia, gdy potrzebujesz PDF na edge.

Przeniosłeś usługę faktur, etykiet albo potwierdzeń do Cloudflare Workers, ponieważ reszta stosu już tam działa, a matematyka latencji wygląda pięknie: 5 ms do najbliższego colo, 1 ms CPU, żądanie zakończone.

Potem trafia do Ciebie generowanie PDF i nagle patrzysz na 800 ms p99, ostrzeżenia o bundle workera 50 MB oraz uporczywe wrażenie, że używasz złego narzędzia. Oto dlaczego tak się dzieje i które faktyczne wąskie gardła możesz ominąć w jedno popołudnie.

Workers to nie Lambda — i to ma znaczenie

Zanim zaczniesz diagnozę, ustaw właściwy model runtime. Cloudflare Workers NIE są kontenerami serverless. To izolaty V8 z następującymi ograniczeniami:

  • Limit czasu CPU: 50 ms na żądanie w planie bezpłatnym, 30 sekund w Workers Paid (Bundled), 5 minut w Unbound. Czas zegarowy jest nielimitowany, ale płatny.
  • Limit pamięci: 128 MB na izolat.
  • Rozmiar bundle: 1 MB w planie bezpłatnym, 10 MB w płatnym.
  • Brak systemu plików. Nie ma fs.readFileSync. Wszystko jest w pamięci albo pobierane.
  • Brak natywnych binariów. Tylko czysty JavaScript / WebAssembly — bez node-canvas, natywnych wywołań zlib i shellowania do Ghostscript.
  • Cold start: około 5 ms. Zaskakująco szybki — właśnie dlatego, że nie ma nic dużego do uruchomienia.

Większość problemów “wolny PDF w Workers” wynika z naruszenia jednego z tych ograniczeń, zwykle limitu CPU albo rozmiaru bundle, i trafienia na ciche dławienie.

Pięć rzeczy, które naprawdę są wolne

W przybliżonej kolejności tego, jak często gryzą zespoły:

1. Wciąganie silników opartych na Chromium do Workers

To po prostu nie działa. Puppeteer potrzebuje około 250 MB Chromium i prawdziwego systemu operacyjnego. Usługi renderowania przeglądarkowego, takie jak Browser Rendering API Cloudflare albo Browserless, działają, ale nie są Workers — to osobna usługa, którą wywołujesz Z Workera, płacąc około 500 ms round trip + czas renderowania.

Jeśli Twój “PDF oparty na Workerze” to w praktyce “Worker wywołujący zdalne API renderowania przeglądarkowego”, dolna granica latencji wynosi około 500 ms. To nie problem Workera; to podatek przeglądarki, który poszedł za Tobą na edge.

Diagnoza: sprawdź, czy kod robi fetch("https://browser-rendering.cloudflare.com/...") albo coś podobnego. Jeśli tak, mierzona latencja należy do usługi upstream, nie do Workera.

2. Robienie układu w JavaScript

Jeśli napisałeś własny silnik układu w JS: “po prostu policzę pozycje boxów ręcznie”, uderzasz w limit CPU. JS jest szybki, ale układ dla ponad 30 elementów z zawijaniem tekstu łatwo przekracza 50 ms w Workers Free oraz 100-300 ms w Bundled.

Pipeline renderowania w kształcie:

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

…wykonuje cztery CPU-bound przebiegi po tych samych danych. Każdy w JS, każdy z kosztem garbage collection, każdy realokuje pośrednie drzewa.

Diagnoza: znajdź log wrangler tail dla renderowania. Jeśli widzisz ponad 50 ms CPU przed jakimkolwiek I/O, to problem compute.

3. Ładowanie fontów przy każdym żądaniu

Fonty mają 50-250 KB każdy. Jeśli silnik renderowania czyta je z KV / R2 przy każdym renderowaniu, dostajesz jeden network round trip na font, na żądanie. Pięć fontów = pięć RTT = 50-150 ms, zanim renderowanie się zacznie.

Diagnoza: dodaj timing do kodu ładowania fontów. Jeśli dominuje p50, to jest problem.

Fix: ładuj fonty raz podczas inicjalizacji modułu: na górze pliku Workera, NIE wewnątrz handlera żądania. Izolat buforuje bajty przez czas swojego życia: od minut do godzin.

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

Jeśli bundler inline’uje font jako bajty, jeszcze lepiej — nie ma I/O w ogóle.

4. Używanie biblioteki PDF w JS, która nie była projektowana dla Workers

pdfkit, pdf-lib, jsPDF działają w Workers, ale mają cechy, które bolą:

  • pdfkit wymaga shimów Node Buffer. To możliwe, ale dodaje około 500 KB i spowalnia compute o około 30%.
  • pdf-lib jest świetne do edytowania istniejących PDF, mniej dobre do emisji od zera — warstwa abstrakcji dodaje około 10 ms narzutu na stronę.
  • jsPDF jest browser-first; ma ten sam problem z Buffer oraz rozległe API, które trudno tree-shake’ować.

Dla pipeline’u, który głównie “czyta JSON, pisze bajty PDF”, wyspecjalizowany silnik emitujący PDF bezpośrednio, bez przechodzenia przez ogólną abstrakcję PDF, będzie 5-20× szybszy. Silniki WebAssembly kompilowane z Rust albo C++ korzystają dodatkowo z ciasnej pętli przyjaznej JIT.

5. Bundle, który po cichu ma 4 MB

Workers Free ogranicza bundle do 1 MB. Workers Bundled do 10 MB. Większość zespołów odkrywa limit, gdy wrangler deploy odrzuca wdrożenie komunikatem “Script exceeds size limit”. Inni zauważają to wcześniej, gdy ogromny import spowalnia cold start, bo V8 musi wszystko skompilować.

wrangler pokaże rozmiar bundle w wyniku deploy. Wszystko powyżej 500 KB zasługuje na sprawdzenie. Typowi winowajcy:

  • Bundlowane fonty. Przenieś je do Workers Assets i pobierz raz podczas inicjalizacji modułu.
  • Warstwa shimów node:. Jeśli w source map widzisz __cf_KV albo polyfills:, bundler podkłada Node API, których prawdopodobnie nie potrzebujesz.
  • Nieużywane zależności. npm run build -- --analyze w Wrangler 4+ daje treemap.

Jak naprawdę wygląda szybki PDF w Workers

Wyspecjalizowany silnik renderowania na edge dla dokumentów strukturalnych (gPdf jest jednym przykładem, ale architektura dotyczy każdego dobrze zaprojektowanego silnika):

Metryka Typowo Dlaczego
Cold start 5-20 ms Boot izolatu V8 + pierwsze ładowanie modułu WASM
CPU na renderowanie 1-4 ms Ciasna pętla WASM, bez presji GC
Czas zegarowy na renderowanie 3-8 ms CPU + kilka µs kryptografii dla identyfikatorów obiektów PDF
Rozmiar bundle 4-6 MB Silnik + bundlowane fonty (Latin + CJK NotoSans)
Szczyt pamięci 8-20 MB Drzewo dokumentu + bufor emitowanego PDF

Porównaj to z typową ścieżką “Puppeteer-on-Workers przez zdalne renderowanie przeglądarkowe”: p50 500-1000 ms, 1-2 GB pamięci przeglądarki hostowanej gdzie indziej i około 0,001 USD/render kosztu upstream.

Szybki triage

Jeśli teraz trafiasz na wolny PDF w Workers, przejdź tę listę przed czymkolwiek innym:

  1. Gdzie idzie czas? Dodaj timestampy w wrangler tail. Ustal, czy wąskie gardło to:
    • CPU: praca w procesie,
    • egress fetch: wywołanie usługi upstream,
    • cold start: tylko pierwsze żądanie po okresie ciszy.
  2. Czy uruchamiasz układ w JS? Jeśli tak, to większość CPU. Przejdź na silnik renderowania, który wstępnie liczy układ.
  3. Czy ładujesz fonty na żądanie? Przenieś ładowanie fontów do inicjalizacji modułu.
  4. Czy wywołujesz zewnętrzną przeglądarkę? Wtedy dolna granica latencji to czas odpowiedzi tej usługi. Przejdź na silnik w tym samym izolacie, bez fetch.
  5. Czy bundle przekracza 1 MB? Cold start skaluje się z rozmiarem bundle. Przytnij nieużywane zależności.

Najszybszy możliwy PDF w Workers to taki, w którym dane dokumentu → bajty PDF powstają w całości w pojedynczym wywołaniu handlera fetch, bez własnych wywołań fetch() i bez ciężkiego układu w JS. Większość skarg “Workers są wolne do PDF” oznacza w praktyce: “włożyliśmy do Workers stos PDF niepasujący do tego runtime i dostaliśmy najgorsze cechy obu światów”.

Krótka wersja

Cloudflare Workers mogą renderować PDF w pojedynczych milisekundach — ale tylko wtedy, gdy silnik renderowania jest zbudowany pod runtime w kształcie izolatu. Biblioteki PDF w JS projektowane dla Node, usługi renderowania przeglądarkowego wywoływane z Workera, fonty ładowane przy każdym żądaniu, przebiegi układu w JavaScript: każdy z tych elementów ustawia p50 gdzieś między “wolniej niż Twój origin” a “nieodróżnialne od regionalnej Lambda”.

Jeśli wolisz nie inżynierować wyjścia samodzielnie, gPdf Playground to w pełni wdrożony na edge silnik renderowania działający dokładnie na tym runtime. Kliknij Render PDF, zobacz zakładkę Network — takie p50 potrafi osiągnąć Worker, gdy nic w pipeline nie walczy ze środowiskiem.