Вы перенесли сервис счетов, этикеток или квитанций в Cloudflare Workers, потому что остальная инфраструктура уже там. По latency все выглядело красиво: 5 ms до ближайшего colo, 1 ms CPU, запрос завершен.
Потом появилась генерация 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 на запрос в Free, 30 seconds в Workers Paid Bundled, 5 minutes в Unbound. Wall time может быть больше, но он billable.
- Memory cap: 128 MB на isolate.
- Bundle size: 1 MB в Free, 10 MB в Paid.
- Нет filesystem. Нет
fs.readFileSync; все в памяти или через fetch. - Нет native binaries. Только pure 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 плюс время рендера.
Если ваш “PDF в Worker” на деле “Worker вызывает удаленный браузер”, нижняя граница latency около 500 ms. Это не проблема Worker, это налог браузера.
Диагностика: проверьте, нет ли fetch("https://browser-rendering.cloudflare.com/...") или аналога. Если есть, вы меряете upstream, а не Worker.
2. Layout на JavaScript
Если вы написали свой JS layout engine, который считает позиции боксов и переносы текста, вы упираетесь в CPU. JS быстрый, но layout для 30+ элементов с text wrapping легко выходит за 50 ms на Workers Free и может дать 100-300 ms на Bundled.
Такая pipeline дорогая:
JSON → JS layout pass → SVG generation → SVG-to-PDF library → emit
Четыре CPU-bound прохода по одним данным, все в JS, с GC pressure и промежуточными деревьями.
Диагностика: посмотрите wrangler tail для одного render. Если до любого I/O уже больше 50 ms CPU, это compute bottleneck.
3. Загрузка шрифтов на каждый запрос
Шрифт весит 50-250 KB. Если renderer читает их из KV / R2 на каждый render, вы добавляете network round-trip на каждый шрифт и запрос. Пять шрифтов — это 50-150 ms до начала рендера.
Диагностика: добавьте timing вокруг загрузки шрифтов. Если она доминирует p50, причина найдена.
Исправление: грузите шрифты один раз на module init, в верхнем уровне 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 инлайнит шрифт как bytes, еще лучше: ноль I/O.
4. JS PDF library, которая не была сделана для Workers
pdfkit, pdf-lib, jsPDF запускаются в Workers, но с компромиссами:
pdfkitтребует NodeBuffershims. Работает, но добавляет около 500 KB и замедляет compute примерно на 30%.pdf-libхорош для редактирования существующих PDF, но хуже для генерации с нуля; abstraction layer добавляет примерно 10 ms на страницу.jsPDFbrowser-first, с той же проблемой Buffer и большим API surface, который трудно tree-shake.
Для pipeline “прочитать JSON, записать PDF bytes” специализированный engine, который пишет 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”. Иногда это видно раньше по холодному старту: V8 должен компилировать слишком много кода.
wrangler показывает bundle size в deploy output. Все выше 500 KB стоит проверять:
- Упакованные шрифты. Перенесите их в Workers Assets и fetch один раз на module init.
node:shim layer. Если в source map видны__cf_KVилиpolyfills:, bundler имитирует Node APIs, которые могут быть не нужны.- Неиспользуемые зависимости. В Wrangler 4+
npm run build -- --analyzeдает treemap.
Как выглядит быстрый PDF в Workers
Edge-native renderer для структурированных документов, например 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 вызывает удаленный Puppeteer/browser rendering”: p50 500-1000 ms, 1-2 GB browser memory где-то в другом сервисе и upstream cost около $0.001/render.
Быстрый triage
Если PDF в Workers уже тормозит, сначала пройдите чеклист:
- Куда уходит время? Добавьте timestamps в
wrangler tailи отделите CPU, upstream fetch и cold start. - Есть JS layout? Если да, это, скорее всего, основная часть CPU. Перейдите на renderer внутри isolate.
- Шрифты грузятся на каждый request? Перенесите на module init.
- Вы зовете внешний браузер? Тогда latency floor равен этому сервису. Используйте same-isolate renderer.
- Bundle больше 1 MB? Cold start растет вместе с размером. Уберите лишние deps.
Самый быстрый PDF в Workers — когда document data превращается в PDF bytes внутри одного вызова fetch handler, без внутренних fetch() и без тяжелого JS layout.
Коротко
Cloudflare Workers могут рендерить PDF за single-digit milliseconds, но только если renderer сделан под isolate runtime. JS PDF libs для Node, удаленный browser rendering, загрузка шрифтов на каждый request и layout в JavaScript поднимают p50 до уровня медленнее origin.
Если не хотите строить это сами, попробуйте gPdf Playground. Он работает на таком же edge runtime. Нажмите Render PDF, посмотрите Network tab — это скорость Workers, когда pipeline не спорит со средой.