Blog

PDF lento no Cloudflare Workers? Diagnostique em 5 minutos

Workers são rápidos até receberem uma stack de PDF feita para servidores long-lived. Veja os gargalos reais e como evitá-los no edge.

Você levou seu serviço de faturas, etiquetas ou recibos para Cloudflare Workers porque o resto da stack já roda lá. A conta de latência parecia ótima: 5 ms até o colo mais próximo, 1 ms de CPU e a requisição acaba.

Então entra geração de PDF. O p99 vira 800 ms, aparecem alertas de worker bundle com 50 MB e fica a sensação de que a ferramenta errada foi escolhida. Na maioria das vezes, o problema não é Workers. É uma stack de PDF desenhada para servidores long-lived, rodando dentro de um runtime de isolates.

Workers não é Lambda, e isso importa

Cloudflare Workers não são contêineres serverless. São V8 isolates com limites próprios:

  • Tempo de CPU: 50 ms por requisição no plano Free, 30 segundos em Workers Paid Bundled, 5 minutos em Unbound. Wall time pode ser maior, mas é cobrado.
  • Memória: 128 MB por isolate.
  • Bundle size: 1 MB no Free, 10 MB no Paid.
  • Sem filesystem. Nada de fs.readFileSync; tudo fica em memória ou vem por fetch.
  • Sem binários nativos. Apenas JavaScript puro / WebAssembly; sem node-canvas, zlib nativo ou Ghostscript por shell.
  • Cold start em torno de 5 ms. Muito rápido justamente porque não há nada grande para inicializar.

Quase todo caso de “PDF lento em Workers” viola algum desses limites, principalmente CPU ou bundle size.

As cinco coisas que realmente são lentas

1. Tentar colocar Chromium dentro do Worker

Isso não fecha. Puppeteer precisa de cerca de 250 MB de Chromium e um sistema operacional real. Serviços como Cloudflare Browser Rendering API ou Browserless funcionam, mas não são Workers: são serviços remotos chamados pelo Worker, com cerca de 500 ms de round-trip mais o tempo de render.

Se o seu “PDF no Worker” é “Worker chamando navegador remoto”, seu piso de latência está perto de 500 ms. Isso não é problema do Worker; é o imposto do browser.

Diagnóstico: procure fetch("https://browser-rendering.cloudflare.com/...") ou algo parecido. Se existir, você está medindo o upstream, não o Worker.

2. Fazer layout em JavaScript

Se você escreveu um motor de layout em JS para calcular posições de caixas e quebras de linha, vai bater no limite de CPU. JS é rápido, mas layout para 30+ elementos com text wrapping passa fácil de 50 ms no Workers Free e pode chegar a 100-300 ms no Bundled.

Esta pipeline é cara:

JSON → JS layout pass → SVG generation → SVG-to-PDF library → emit

Ela faz quatro passagens CPU-bound sobre os mesmos dados, todas em JS, com pressão de GC e árvores intermediárias.

Diagnóstico: veja wrangler tail em um render. Se houver mais de 50 ms de CPU antes de qualquer I/O, o gargalo é compute.

3. Carregar fontes em toda requisição

Fontes têm 50-250 KB. Se o renderer lê de KV / R2 em cada render, você adiciona um round-trip por fonte, por request. Cinco fontes são cinco RTTs: 50-150 ms antes do render começar.

Diagnóstico: adicione timing no código de fontes. Se ele domina o p50, achou o problema.

Correção: carregue as fontes uma vez em module init, no topo do arquivo Worker, não dentro do request handler. O isolate mantém os bytes em cache pelo tempo de vida dele.

// 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 });
  }
};

Se o bundler embute a fonte como bytes, melhor ainda: zero I/O.

4. Usar uma biblioteca PDF de JS que não nasceu para Workers

pdfkit, pdf-lib e jsPDF rodam em Workers, mas cobram seu preço:

  • pdfkit precisa de shims de Node Buffer; funciona, mas adiciona cerca de 500 KB e pode deixar o compute 30% mais lento.
  • pdf-lib é ótima para editar PDFs existentes, menos para emitir do zero; sua abstração soma cerca de 10 ms por página.
  • jsPDF é browser-first, tem o mesmo problema de Buffer e uma API grande difícil de tree-shake.

Para uma pipeline “ler JSON, escrever bytes PDF”, um motor dedicado que emite PDF direto costuma ser 5-20 vezes mais rápido. Rust ou C++ compilado para WebAssembly se beneficia de loops pequenos e previsíveis.

5. O bundle que virou 4 MB sem alarde

Workers Free limita o bundle a 1 MB; Workers Bundled, a 10 MB. Muitos times descobrem no wrangler deploy, quando vem “Script exceeds size limit”. Outros percebem antes, com cold starts mais lentos, porque o V8 precisa compilar código demais.

O wrangler mostra o bundle size no deploy. Qualquer coisa acima de 500 KB merece investigação:

  • Fontes empacotadas. Mova para Workers Assets e faça fetch uma vez em module init.
  • Shims de node:. Se o source map mostra __cf_KV ou polyfills:, o bundler está simulando APIs Node que talvez você nem use.
  • Dependências não usadas. No Wrangler 4+, npm run build -- --analyze mostra um treemap.

Como é um PDF rápido em Workers

Um renderizador nativo da edge para documentos estruturados, como o gPdf, costuma ficar nesta faixa:

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

Compare com o caminho típico “Worker chama Puppeteer remoto”: p50 de 500-1000 ms, 1-2 GB de memória do navegador em outro serviço e custo upstream perto de $0.001/render.

Triage rápido

Se você está com PDF lento em Workers agora, rode esta lista antes de trocar tudo:

  1. Onde o tempo vai? Coloque timestamps em wrangler tail e separe CPU, fetch para upstream e cold start.
  2. Há layout em JS? Se sim, provavelmente é a maior parte do CPU. Use um renderer dentro do isolate.
  3. As fontes carregam por request? Mova para module init.
  4. Você chama um navegador externo? Então a latência mínima é a desse serviço. Use renderização no mesmo isolate.
  5. O bundle passa de 1 MB? Cold start cresce com o tamanho. Corte dependências.

O PDF mais rápido em Workers é aquele em que os dados do documento viram bytes PDF dentro de uma única chamada ao fetch handler, sem fetch() interno e sem layout pesado em JS.

Resumo

Cloudflare Workers consegue renderizar PDF em milissegundos de um dígito, mas só quando o renderer foi feito para um runtime de isolate. Bibliotecas JS feitas para Node, browsers remotos, fontes carregadas por request e layout em JavaScript empurram seu p50 para algo mais lento que a origin.

Se você não quer construir essa saída sozinho, teste o gPdf Playground. Ele roda nesse mesmo tipo de runtime edge. Clique em Render PDF e veja a aba Network; é assim que Workers se comporta quando a pipeline não briga com o ambiente.