Movió su servicio de facturas, etiquetas o recibos a Cloudflare Workers porque el resto de su pila ya vive allí. La cuenta de latencia parecía perfecta: 5 ms hasta la colo más cercana, 1 ms de CPU y solicitud terminada.
Entonces llegó la generación de PDF: p99 de 800 ms, avisos de bundle de 50 MB y la sensación persistente de haber elegido la herramienta equivocada. Normalmente el problema no es Workers; es una pila PDF diseñada para servidores de larga vida, metida en un entorno de ejecución de isolates.
Workers no es Lambda, y eso cambia el diagnóstico
Antes de diagnosticar, conviene tener claro el modelo de ejecución. Cloudflare Workers NO son contenedores serverless. Son isolates V8 con estos límites:
- Tiempo de CPU: 50 ms por solicitud en el plan Free, 30 segundos en Workers Paid (Bundled), 5 minutos en Unbound. El tiempo total puede ser mayor, pero se factura.
- Memoria: 128 MB por isolate.
- Tamaño del bundle: 1 MB en Free, 10 MB en el plan de pago.
- Sin sistema de archivos. No hay
fs.readFileSync; todo está en memoria o se obtiene por red. - Sin binarios nativos. Solo JavaScript puro o WebAssembly; nada de
node-canvas, zlib nativo ni Ghostscript por shell. - Arranque en frío de ~5 ms. Sorprendentemente rápido, pero solo porque no hay nada grande que arrancar.
La mayoría de problemas de “PDF lento en Workers” vienen de violar uno de esos límites, normalmente el límite de CPU o el tamaño del bundle, y de recibir throttling silencioso.
Las cinco cosas que realmente ralentizan
1. Intentar llevar Chromium a Workers
Esto no funciona, punto. Puppeteer necesita unos 250 MB de Chromium y un sistema operativo real. Los servicios de renderizado de navegador, como Cloudflare Browser Rendering API o Browserless, funcionan, pero no son Workers: son un servicio separado al que llama DESDE un Worker, pagando ~500 ms de ida y vuelta más el tiempo de renderizado.
Si su “PDF en Worker” es en realidad “Worker que llama a una API remota de renderizado de navegador”, su suelo de latencia ronda 500 ms. No es un problema de Workers; es el impuesto del navegador siguiéndole hasta el edge.
Diagnóstico: busque fetch("https://browser-rendering.cloudflare.com/...") o algo parecido. Si existe, la latencia que mide es la del servicio externo, no la del Worker.
2. Hacer maquetación en JavaScript
Si escribió su propio motor de maquetación en JS para calcular cajas y saltos de línea, chocará con el límite de CPU. JS es rápido, pero maquetar más de 30 elementos con ajuste de texto supera fácilmente 50 ms en Workers Free y puede irse a 100-300 ms en Bundled.
Una tubería como esta es cara:
JSON → pasada de maquetación JS → generación SVG → biblioteca SVG a PDF → emisión
…hace cuatro pasadas ligadas a CPU sobre los mismos datos. Cada una en JS, cada una con sobrecarga de recolección de basura, cada una reasignando árboles intermedios.
Diagnóstico: revise el log de wrangler tail para un renderizado. Si ve más de 50 ms de CPU antes de cualquier I/O, es un problema de cómputo.
3. Cargar fuentes en cada petición
Una fuente pesa 50-250 KB. Si el renderizador la lee desde KV o R2 en cada renderizado, añade un round-trip de red por fuente y por solicitud. Cinco fuentes son cinco RTT: 50-150 ms antes de empezar.
Diagnóstico: añada mediciones al código de carga de fuentes. Si domina el p50, este es el problema.
Solución: cargue las fuentes una vez en la inicialización del módulo, al principio del archivo Worker, no dentro del manejador de solicitudes. El isolate conserva esos bytes durante su vida útil, de minutos a horas.
// 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 });
}
};
Si su bundler las inserta como bytes, mejor todavía: cero I/O.
4. Usar una librería PDF de JS que no nació para Workers
pdfkit, pdf-lib y jsPDF pueden correr en Workers, pero tienen costes:
pdfkitnecesita shims de NodeBuffer; funciona, pero añade unos 500 KB y ralentiza el cómputo alrededor de 30%.pdf-libes excelente editando archivos PDF existentes, pero menos eficiente emitiendo desde cero; su abstracción añade unos 10 ms por página.jsPDFes browser-first, arrastra el mismo problema de Buffer y una API grande difícil de podar.
Para una canalización que es básicamente “leer JSON y escribir bytes PDF”, un motor dedicado que emite PDF directamente, sin pasar por una abstracción PDF genérica, suele ser 5-20× más rápido. Los motores WebAssembly compilados desde Rust o C++ se benefician aún más de bucles estrechos favorables al JIT.
5. Un bundle que en secreto pesa 4 MB
Workers Free limita el bundle a 1 MB; Workers Bundled, a 10 MB. Muchos equipos descubren el límite cuando wrangler deploy falla con “Script exceeds size limit”. Otros lo notan antes, cuando un import enorme ralentiza el arranque en frío porque V8 tiene que compilarlo todo.
wrangler muestra el tamaño del bundle al desplegar. Cualquier cosa sobre 500 KB merece revisión:
- Fuentes empaquetadas. Muévalas a Workers Assets y haga un
fetchuna vez durante la inicialización del módulo. - Adaptadores de
node:. Si el source map muestra__cf_KVopolyfills:, su bundler está simulando APIs de Node que quizá no necesita. - Dependencias sin usar. En Wrangler 4+,
npm run build -- --analyzeentrega un treemap.
Cómo se ve un PDF rápido en Workers
Un renderizador pensado para el edge y para documentos estructurados — gPdf es un ejemplo, pero la arquitectura aplica a cualquier implementación bien hecha — suele estar en este orden de magnitud:
| Métrica | Típico | Por qué |
|---|---|---|
| Arranque en frío | 5-20 ms | Arranque de isolate V8 + primera carga del módulo WASM |
| CPU por renderizado | 1-4 ms | Bucle estrecho WASM, sin presión de GC |
| Tiempo total por renderizado | 3-8 ms | CPU + unos microsegundos de criptografía para IDs de objetos PDF |
| Tamaño del bundle | 4-6 MB | Renderizador + fuentes incluidas (Latin + CJK NotoSans) |
| Pico de memoria | 8-20 MB | Árbol del documento + buffer PDF emitido |
Compárelo con la ruta típica “Puppeteer en Workers mediante renderizado remoto de navegador”: 500-1000 ms p50, 1-2 GB de memoria de navegador alojada en otro servicio y alrededor de 0,001 USD/renderizado de coste externo.
Triaje rápido
Si ahora mismo tiene PDF lentos en Workers, ejecute esta lista antes de cambiar arquitectura:
- ¿Dónde se va el tiempo? Añada timestamps en
wrangler tail. Determine si el cuello de botella es CPU, unfetchde salida a un servicio externo o arranque en frío. - ¿Hay maquetación en JS? Si la hay, probablemente es la mayor parte de la CPU. Cambie a un renderizador que precalcule la maquetación.
- ¿Carga fuentes por solicitud? Mueva la carga de fuentes a la inicialización del módulo.
- ¿Llama a un navegador externo? Entonces su suelo de latencia es el tiempo de respuesta de ese servicio. Use un renderizador en el mismo isolate, sin
fetch. - ¿El bundle supera 1 MB? El arranque en frío escala con el tamaño del bundle. Recorte dependencias sin usar.
El PDF más rápido posible en Workers es aquel donde los datos del documento se convierten en bytes PDF dentro de una sola llamada al manejador fetch, sin llamadas fetch() internas y sin maquetación pesada en JS.
Versión corta
Cloudflare Workers puede renderizar PDF en milisegundos de un dígito, pero solo si el renderizador está diseñado para un entorno de isolates. Librerías PDF en JS diseñadas para Node, servicios de renderizado de navegador llamados desde un Worker, fuentes cargadas por solicitud y pasadas de maquetación en JavaScript: cada una empuja su p50 hacia algo entre “más lento que su servidor de origen” e “indistinguible de una Lambda regional”.
Si prefiere no diseñar su propia salida para escapar de esto, el gPdf Playground es un renderizador completamente desplegado en el edge que corre sobre este mismo tipo de entorno de ejecución. Pulse Render PDF, mire la pestaña Network y verá lo que puede hacer un Worker cuando nada en la canalización pelea con el entorno.