Ви перенесли сервіс інвойсів, етикеток або квитанцій у Cloudflare Workers, бо решта stack вже працює там. Математика latency виглядала чудово: 5 ms до найближчого colo, 1 ms CPU, request завершено.
Потім з’явилась генерація PDF. p99 став 800 ms, worker bundle попереджає про 50 MB, і здається, що runtime обрано неправильно. Зазвичай проблема не в Workers, а в PDF-стеку для довгоживучих серверів, який поклали в isolate runtime.
Workers — це не Lambda
Cloudflare Workers — не serverless containers. Це V8 isolates з іншими обмеженнями:
- CPU time limit: 50 ms на request у Free plan, 30 seconds у Workers Paid Bundled, 5 minutes в Unbound. Wall time може бути довшим, але billable.
- Memory cap: 128 MB на isolate.
- Bundle size: 1 MB у Free plan, 10 MB у paid.
- Немає filesystem. Немає
fs.readFileSync; усе в memory або через fetch. - Немає нативних бінарних файлів. Лише чистий JavaScript / WebAssembly; без
node-canvas, native zlib чи Ghostscript через shell. - Cold start близько 5 ms. Дуже швидко, бо немає чого великого запускати.
Більшість проблем “PDF повільний у Workers” порушують одне з цих обмежень, найчастіше CPU або bundle size.
П’ять речей, які реально гальмують
1. Спроба принести Chromium у Workers
Це не працює. Puppeteer потребує приблизно 250 MB Chromium і справжню ОС. Cloudflare Browser Rendering API або Browserless можуть рендерити, але це не Workers: Worker викликає зовнішній сервіс і платить приблизно 500 ms round-trip плюс render time.
Якщо ваш “PDF у Worker” насправді “Worker викликає remote browser”, latency floor близько 500 ms. Це не проблема Worker, а browser tax.
Діагностика: шукайте fetch("https://browser-rendering.cloudflare.com/...") або подібний виклик. Якщо він є, ви міряєте upstream latency.
2. Layout у JavaScript
Якщо ви написали власний JS-макет engine для позицій блоків і переносу тексту, ви впираєтесь у ліміт CPU. JS швидкий, але layout для 30+ елементів із wrapping легко перевищує 50 ms у Workers Free і може дати 100-300 ms у Bundled.
Така pipeline дорога:
JSON → JS-макет pass → SVG generation → SVG-to-PDF library → emit
Чотири CPU-bound проходи по тих самих даних, усі в JS, з GC pressure і проміжними деревами.
Діагностика: подивіться wrangler tail для одного render. Якщо до будь-якого I/O вже понад 50 ms CPU, bottleneck — compute.
3. Завантаження шрифтів на кожен request
Шрифти мають 50-250 KB. Якщо renderer читає їх із KV / R2 на кожен render, ви додаєте network round-trip на кожен font і request. П’ять шрифтів — це 50-150 ms до старту render.
Діагностика: додайте timing до font-loading code. Якщо він домінує p50, проблема тут.
Fix: завантажуйте шрифти один раз на module init, у top level Worker-файла, не в request handler. Isolate кешує bytes на час свого життя.
// 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 });
}
};
Якщо bundler inline-ить font як bytes, ще краще: нуль I/O.
4. JS PDF library, не створена для Workers
pdfkit, pdf-lib, jsPDF запускаються у Workers, але мають ціну:
pdfkitпотребує NodeBuffershims. Працює, але додає близько 500 KB і сповільнює compute приблизно на 30%.pdf-libдобре редагує наявні PDFs, але гірше генерує з нуля; abstraction layer додає близько 10 ms на page.jsPDFbrowser-first, має ту саму Buffer-проблему і великий API surface, який важко tree-shake.
Для pipeline “прочитати JSON, записати PDF bytes” спеціалізований engine, що emit-ить PDF напряму, часто у 5-20 разів швидший. Rust або C++ у WebAssembly добре пасують до tight loops.
5. Bundle непомітно став 4 MB
Workers Free обмежує bundle 1 MB, Workers Bundled — 10 MB. Багато команд дізнаються про це, коли wrangler deploy падає з “Script exceeds size limit”. Інколи це видно раніше як повільніший cold start, бо V8 має compile надто багато коду.
wrangler показує bundle size у deploy output. Все понад 500 KB варто перевірити:
- Bundled fonts. Перенесіть у Workers Assets і fetch один раз на module init.
node:shim layer. Якщо source map показує__cf_KVабоpolyfills:, bundler shim-ить Node APIs, які можуть бути не потрібні.- Unused dependencies. У Wrangler 4+
npm run build -- --analyzeдає treemap.
Як виглядає швидкий PDF у Workers
Edge-native renderer для structured documents, наприклад gPdf, зазвичай має такі числа:
| Metric | Typical | Why |
|---|---|---|
| Cold-start | 5-20 ms | V8 isolate boot + WASM module first-load |
| Per-render CPU | 1-4 ms | WASM tight loop, no GC pressure |
| Per-render wall | 3-8 ms | CPU + a few microseconds of crypto for PDF object IDs |
| Bundle size | 4-6 MB | Renderer + bundled fonts (Latin + CJK NotoSans) |
| Memory peak | 8-20 MB | Document tree + emitted PDF buffer |
Порівняйте з типовим “Worker викликає remote Puppeteer/browser rendering”: p50 500-1000 ms, 1-2 GB browser memory в іншому сервісі і upstream cost близько $0.001/render.
Швидкий triage
Якщо PDF у Workers зараз повільний, спочатку пройдіть checklist:
- Куди йде час? Додайте timestamps у
wrangler tailі розділіть CPU, upstream fetch та cold start. - Є JS-макет? Якщо так, це, ймовірно, основний CPU. Використайте renderer у тому ж isolate.
- Шрифти вантажаться на request? Перенесіть у module init.
- Викликаєте external browser? Latency floor — цей service. Перейдіть на same-isolate rendering.
- Bundle понад 1 MB? Cold start росте разом із bundle size. Приберіть unused deps.
Найшвидший PDF у Workers — коли дані документа стають PDF-байтами всередині одного виклику fetch handler, без внутрішніх fetch() і без важкого JS-макет.
Коротко
Cloudflare Workers можуть render-ити PDF за однозначні мілісекунди, але лише якщо renderer зроблений для isolate runtime. JS PDF libs для Node, віддалені браузерні сервіси, fonts на кожен request і JavaScript layout піднімають p50 до рівня повільнішого за origin.
Якщо не хочете будувати це самі, спробуйте gPdf Playground. Він працює на такому самому edge runtime. Натисніть Render PDF і подивіться вкладку Network; це швидкість Workers, коли pipeline не воює з середовищем.