Blog

PDF lent dans Cloudflare Workers ? Diagnostic en 5 minutes

Workers est rapide, jusqu'au moment où vous lui confiez une pile PDF pensée pour des serveurs persistants. Les vrais goulots d'étranglement, et comment les éviter à l'edge.

Vous avez déplacé votre service de factures, d’étiquettes ou de reçus vers Cloudflare Workers parce que le reste de votre stack y tourne déjà, et que le calcul de latence semble idéal : 5 ms jusqu’au colo le plus proche, 1 ms de CPU, requête terminée.

Puis la génération de PDF arrive, et vous vous retrouvez soudain avec des p99 à 800 ms, des alertes de bundle Worker à 50 MB, et la sensation persistante d’utiliser le mauvais outil. Voici pourquoi cela arrive, et les vrais goulots d’étranglement que vous pouvez corriger en un après-midi.

Workers n’est pas Lambda, et cela compte

Avant de diagnostiquer, il faut avoir le bon modèle d’exécution en tête. Cloudflare Workers n’est PAS un conteneur serverless. Ce sont des V8 isolates avec des contraintes précises :

  • Limite de temps CPU : 50 ms par requête sur le plan gratuit, 30 secondes sur Workers Paid (Bundled), 5 min sur Unbound. Le wall time n’est pas borné, mais il est facturé.
  • Plafond mémoire : 128 MB par isolate.
  • Taille du bundle : 1 MB sur le plan gratuit, 10 MB sur le plan payant.
  • Pas de système de fichiers. Pas de fs.readFileSync. Tout est en mémoire ou récupéré par réseau.
  • Pas de binaires natifs. JavaScript pur / WebAssembly uniquement : pas de node-canvas, pas d’appels zlib natifs, pas de Ghostscript lancé en shell.
  • Cold start : environ 5 ms. Extrêmement rapide, justement parce qu’il n’y a rien de lourd à démarrer.

La plupart des problèmes de “PDF lent dans Workers” viennent d’une de ces contraintes qui est violée, généralement la limite CPU ou la taille du bundle, puis d’un ralentissement difficile à voir.

Les cinq choses qui sont réellement lentes

Dans l’ordre approximatif où elles touchent le plus souvent les équipes :

1. Essayer d’amener un moteur basé sur Chromium dans Workers

Cela ne fonctionne pas, point. Puppeteer a besoin d’environ 250 MB de Chromium et d’un vrai OS. Les services de rendu navigateur, comme Browser Rendering API de Cloudflare ou Browserless, fonctionnent, mais ce ne sont pas des Workers : ce sont des services séparés que vous appelez DEPUIS un Worker, en payant environ 500 ms d’aller-retour plus le temps de rendu.

Si votre “PDF dans Worker” est en réalité “un Worker qui appelle une API distante de rendu navigateur”, votre plancher de latence est autour de 500 ms. Ce n’est pas un problème Worker ; c’est la taxe navigateur qui vous suit jusqu’à l’edge.

Diagnostic : vérifiez si votre code fait fetch("https://browser-rendering.cloudflare.com/...") ou un appel similaire. Si oui, la latence mesurée appartient au service amont, pas au Worker.

2. Faire la mise en page en JavaScript

Si vous avez écrit votre propre moteur de mise en page en JS, par exemple “je calcule juste les positions de boîtes à la main”, vous heurtez la limite CPU. JavaScript est rapide, mais la mise en page de plus de 30 éléments avec retours à la ligne dépasse facilement 50 ms sur Workers Free, et peut monter à 100-300 ms sur Bundled.

Un pipeline de rendu qui ressemble à ceci :

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

…effectue quatre passes liées au CPU sur les mêmes données. Chacune en JS, avec coût de garbage collection, et chacune réalloue des arbres intermédiaires.

Diagnostic : retrouvez votre journal wrangler tail pour un rendu. Si vous voyez plus de 50 ms de CPU avant toute I/O, c’est un problème de compute.

3. Charger les polices à chaque requête

Une police pèse 50-250 KB. Si votre moteur les lit depuis KV / R2 à chaque rendu, cela ajoute un aller-retour réseau par police et par requête. Cinq polices = cinq RTT = 50-150 ms avant même que le rendu commence.

Diagnostic : ajoutez des mesures autour du chargement des polices. Si cela domine le p50, le problème est là.

Correction : chargez les polices une seule fois à l’initialisation du module, en haut du fichier Worker, PAS dans le gestionnaire de requête. L’isolate garde ces octets pendant sa durée de vie, de quelques minutes à quelques heures.

// 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 votre bundler intègre la police directement en octets, c’est encore mieux : aucune I/O.

4. Utiliser une bibliothèque PDF JS qui n’a pas été pensée pour Workers

pdfkit, pdf-lib, jsPDF fonctionnent tous dans Workers, mais leurs caractéristiques coûtent cher :

  • pdfkit exige des shims Node Buffer. C’est possible, mais cela ajoute environ 500 KB et ralentit le compute d’environ 30 %.
  • pdf-lib est excellent pour modifier des PDF existants, moins pour les émettre depuis zéro : sa couche d’abstraction ajoute environ 10 ms de surcharge par page.
  • jsPDF est conçu d’abord pour le navigateur ; même problème de Buffer, plus une surface API large et difficile à tree-shaker.

Pour un pipeline qui fait surtout “lire du JSON, écrire des octets PDF”, un moteur spécialisé qui émet directement le PDF, sans passer par une abstraction PDF générique, sera 5 à 20 fois plus rapide. Les moteurs WebAssembly compilés depuis Rust ou C++ profitent en plus de boucles serrées favorables au JIT.

5. Le bundle qui pèse secrètement 4 MB

Workers Free limite le bundle à 1 MB. Workers Bundled le limite à 10 MB. Beaucoup d’équipes découvrent la limite quand wrangler deploy refuse avec “Script exceeds size limit”. D’autres le sentent plus tôt, quand un gros import ralentit le cold start parce que V8 doit tout compiler.

wrangler affiche la taille du bundle dans sa sortie de déploiement. Au-dessus de 500 KB, cela mérite inspection. Coupables fréquents :

  • Polices incluses dans le bundle. Déplacez-les vers Workers Assets et récupérez-les une seule fois à l’initialisation du module.
  • Couche de shims node:. Si vous voyez __cf_KV ou polyfills: dans la source map, votre bundler simule des API Node dont vous n’avez peut-être pas besoin.
  • Dépendances inutilisées. npm run build -- --analyze avec Wrangler 4+ donne une treemap.

À quoi ressemble un PDF rapide dans Workers

Un moteur de rendu à l’edge conçu pour des documents structurés, gPdf par exemple, ressemble généralement à ceci :

Métrique Typique Pourquoi
Cold start 5-20 ms Démarrage V8 isolate + premier chargement du module WASM
CPU par rendu 1-4 ms Boucle serrée WASM, pas de pression GC
Wall time par rendu 3-8 ms CPU + quelques microsecondes de crypto pour les ID d’objets PDF
Taille du bundle 4-6 MB Moteur + polices intégrées (Latin + CJK NotoSans)
Pic mémoire 8-20 MB Arbre documentaire + buffer PDF émis

Comparez avec le chemin typique “Puppeteer-on-Workers via rendu navigateur distant” : p50 à 500-1000 ms, 1-2 GB de mémoire navigateur hébergée ailleurs, et environ 0,001 USD/rendu de coût amont.

Triage rapide

Si vous avez un PDF lent dans Workers maintenant, lancez cette checklist avant toute autre chose :

  1. Où part le temps ? Ajoutez des timestamps dans wrangler tail. Établissez si le goulot est :
    • CPU : travail dans le processus
    • fetch sortant : appel à un service amont
    • cold start : seulement la première requête après une période calme
  2. Faites-vous la mise en page en JS ? Si oui, c’est probablement l’essentiel de votre CPU. Passez à un moteur qui pré-calcule la mise en page.
  3. Chargez-vous les polices par requête ? Déplacez le chargement à l’initialisation du module.
  4. Appelez-vous un navigateur externe ? Alors votre plancher de latence est le temps de réponse de ce service. Passez à un moteur dans le même isolate, sans fetch.
  5. Votre bundle dépasse-t-il 1 MB ? Le cold start augmente avec la taille du bundle. Supprimez les dépendances inutilisées.

Le PDF le plus rapide possible dans Workers est celui où vos données de document deviennent des octets PDF entièrement dans un seul appel au gestionnaire fetch, sans aucun fetch() interne et sans mise en page JS lourde. La plupart des plaintes “Workers est lent pour les PDF” que nous entendons signifient en réalité : “nous avons placé une pile PDF qui n’a pas la forme Workers dans Workers, et nous avons obtenu le pire des deux mondes”.

Version courte

Cloudflare Workers peut générer des PDF en quelques millisecondes, mais seulement si le moteur est construit pour un runtime en isolate. Bibliothèques PDF JS conçues pour Node, services de rendu navigateur appelés depuis un Worker, polices chargées par requête, passes de mise en page en JavaScript : chacun de ces choix place votre p50 entre “plus lent que votre origine” et “impossible à distinguer d’une Lambda régionale”.

Si vous préférez ne pas concevoir vous-même cette sortie, le gPdf Playground est un moteur entièrement déployé à l’edge, sur ce même runtime. Cliquez Render PDF, regardez l’onglet réseau : ce p50 montre ce que Workers peut faire quand rien dans le pipeline ne lutte contre l’environnement.