Блог

PDF у Cloudflare Workers генерується повільно? Діагностика за 5 хвилин

Workers швидкі, доки в них не кладуть PDF-стек для довгоживучих серверів. Ось реальні edge-вузькі місця і як їх обійти.

Ви перенесли сервіс інвойсів, етикеток або квитанцій у 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 потребує Node Buffer shims. Працює, але додає близько 500 KB і сповільнює compute приблизно на 30%.
  • pdf-lib добре редагує наявні PDFs, але гірше генерує з нуля; abstraction layer додає близько 10 ms на page.
  • jsPDF browser-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:

  1. Куди йде час? Додайте timestamps у wrangler tail і розділіть CPU, upstream fetch та cold start.
  2. Є JS-макет? Якщо так, це, ймовірно, основний CPU. Використайте renderer у тому ж isolate.
  3. Шрифти вантажаться на request? Перенесіть у module init.
  4. Викликаєте external browser? Latency floor — цей service. Перейдіть на same-isolate rendering.
  5. 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 не воює з середовищем.