نقلت خدمة الفواتير أو الملصقات أو الإيصالات إلى Cloudflare Workers لأن بقية النظام يعمل هناك. الحساب يبدو جميلًا: 5 ms إلى أقرب colo، و1 ms CPU، ثم تنتهي الطلبية.
ثم يصل توليد PDF. فجأة ترى p99 عند 800 ms، وتحذيرات bundle بحجم 50 MB، وشعورًا بأنك تستخدم الأداة الخطأ. إليك لماذا يحدث ذلك، وما الاختناقات التي يمكنك إصلاحها فعليًا في فترة قصيرة.
Workers ليست Lambda، وهذا يغيّر التشخيص
قبل التشخيص، اضبط نموذج التشغيل ذهنيًا. Cloudflare Workers ليست حاويات serverless. هي V8 isolates بقيود واضحة:
- زمن CPU: 50 ms لكل طلب في Free، و30 ثانية في Workers Paid (Bundled)، و5 دقائق في Unbound. wall time غير محدود لكنه billable.
- الذاكرة: 128 MB لكل isolate.
- حجم bundle: 1 MB في Free و10 MB في Paid.
- لا يوجد filesystem. لا يوجد
fs.readFileSync؛ كل شيء في الذاكرة أو عبر fetch. - لا توجد native binaries. JavaScript / WebAssembly فقط؛ لا
node-canvas، ولا zlib native، ولا Ghostscript عبر shell. - Cold start يقارب 5 ms. سريع جدًا لأن لا شيء ضخمًا يحتاج إلى الإقلاع.
معظم مشاكل “PDF بطيء في Workers” تأتي من كسر أحد هذه القيود، غالبًا CPU أو bundle size، ثم الاصطدام بتباطؤ يصعب رؤيته مباشرة.
الأشياء الخمسة البطيئة فعلا
1. إدخال Chromium إلى Workers
هذا لا يعمل، ببساطة. Puppeteer يحتاج تقريبًا 250 MB من Chromium ونظام تشغيل حقيقي. خدمات Browser Rendering API أو Browserless تعمل، لكنها ليست Workers؛ هي خدمات خارجية يستدعيها Worker، مع نحو 500 ms round-trip إضافة إلى زمن العرض.
إذا كان “PDF داخل Worker” يعني “Worker يستدعي متصفحًا بعيدًا”، فحد latency الأدنى لديك قريب من 500 ms. هذا ليس بطء Workers؛ هذه ضريبة المتصفح وقد تبعتك إلى Edge.
التشخيص: ابحث عن fetch("https://browser-rendering.cloudflare.com/...") أو ما يشبهه. إن وجدته، فأنت تقيس خدمة upstream لا Worker.
2. تنفيذ التخطيط في JavaScript
إن كتبت محرك تخطيط في JS لحساب مواضع الصناديق والتفاف النص، فستصطدم بحد CPU. JavaScript سريع، لكن تخطيط أكثر من 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 وبإعادة تخصيص أشجار وسيطة.
التشخيص: افتح wrangler tail لعملية عرض واحدة. إن رأيت أكثر من 50 ms CPU قبل أي I/O، فالمشكلة حسابية.
3. تحميل الخطوط في كل طلب
الخط الواحد غالبًا 50-250 KB. إذا كان العارض يقرأ الخطوط من KV / R2 في كل render، فأنت تضيف 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. استخدام مكتبة PDF في JS لم تُبنَ لـ Workers
pdfkit وpdf-lib وjsPDF يمكن تشغيلها في Workers، لكن لها كلفة:
pdfkitيحتاج NodeBuffershims. يعمل، لكنه يضيف نحو 500 KB ويبطئ compute بنحو 30%.pdf-libممتاز لتعديل PDFs موجودة، لكنه أضعف عند الإنشاء من الصفر؛ طبقة abstraction تضيف نحو 10 ms لكل صفحة.jsPDFbrowser-first، ومعه مشكلة Buffer وسطح API كبير يصعب tree-shake.
عندما يكون المسار هو “اقرأ JSON، اكتب PDF bytes”، فإن محركًا مخصصًا يكتب PDF مباشرة يكون غالبًا أسرع 5-20 مرة. WebAssembly المترجم من Rust أو C++ يستفيد أكثر من الحلقات الضيقة الصديقة لـ JIT.
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 في deploy output. أي شيء فوق 500 KB يستحق المراجعة:
- خطوط مضمّنة. انقلها إلى Workers Assets واعمل fetch مرة واحدة في module init.
- طبقة
node:shim. إذا رأيت__cf_KVأوpolyfills:في source map، فـ bundler يضيف Node APIs لا تحتاجها. - Dependencies غير مستخدمة. في Wrangler 4+ يعطيك
npm run build -- --analyzetreemap.
كيف يبدو PDF السريع في Workers
عارض مبني لـ Edge وللوثائق المنظمة، مثل 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 في خدمة أخرى، وكلفة upstream تقارب $0.001/render.
قائمة فحص سريعة
إن كان PDF بطيئا الآن في Workers، فابدأ بهذا:
- أين يذهب الوقت؟ أضف timestamps في
wrangler tailوافصل CPU عن upstream fetch وعن cold start. - هل تنفذ التخطيط في JS؟ إن كان نعم، فهذا غالبًا معظم CPU. انتقل إلى عارض يحسب التخطيط مسبقًا.
- هل تحمل الخطوط في كل طلب؟ انقلها إلى module init.
- هل تستدعي متصفحًا خارجيًا؟ إذن حد latency هو زمن تلك الخدمة. انتقل إلى عارض داخل isolate نفسه، بلا fetch.
- هل bundle أكبر من 1 MB؟ cold start يكبر مع الحجم. احذف dependencies غير المستخدمة.
أسرع PDF على Workers هو الذي تتحول فيه document data إلى PDF bytes داخل استدعاء واحد لـ fetch handler، بلا fetch() داخلي وبلا تخطيط ثقيل في JS.
المختصر
Cloudflare Workers قادرة على توليد PDFs في single-digit milliseconds، لكن فقط إذا كان العارض مصممًا لبيئة تشغيل تشبه isolate. مكتبات JS المصممة لـ Node، وخدمات المتصفح البعيدة، وتحميل الخطوط في كل طلب، والتخطيط في JavaScript، كلها ترفع p50 حتى يصبح أبطأ من origin.
إن لم ترد هندسة ذلك بنفسك، جرّب gPdf Playground. إنه عارض منشور بالكامل على Edge ويعمل على بيئة التشغيل نفسها. اضغط Render PDF وانظر إلى Network tab؛ هذا ما يستطيع Worker فعله عندما لا يقاتله مسار العمل.