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ą:
pdfkitwymaga shimów NodeBuffer. To możliwe, ale dodaje około 500 KB i spowalnia compute o około 30%.pdf-libjest świetne do edytowania istniejących PDF, mniej dobre do emisji od zera — warstwa abstrakcji dodaje około 10 ms narzutu na stronę.jsPDFjest 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_KValbopolyfills:, bundler podkłada Node API, których prawdopodobnie nie potrzebujesz. - Nieużywane zależności.
npm run build -- --analyzew 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:
- 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.
- Czy uruchamiasz układ w JS? Jeśli tak, to większość CPU. Przejdź na silnik renderowania, który wstępnie liczy układ.
- Czy ładujesz fonty na żądanie? Przenieś ładowanie fontów do inicjalizacji modułu.
- 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.
- 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.