ব্লগ

Cloudflare Workers-এ PDF generation ধীর? ৫ মিনিটে bottleneck ধরুন

Workers দ্রুত, যতক্ষণ না সেখানে long-lived server-এর জন্য বানানো PDF stack বসানো হয়। Edge-এ PDF ধীর করার আসল কারণ এবং কীভাবে এড়াবেন।

আপনি invoice, label বা receipt service Cloudflare Workers-এ এনেছেন, কারণ বাকি stack-ও edge-এ চলে। Latency হিসাব সুন্দর: nearest colo 5 ms, CPU 1 ms, request শেষ।

তারপর PDF generation আসে। p99 800 ms, worker bundle warning 50 MB, আর মনে হয় runtime বোধহয় ভুল। বেশিরভাগ সময় সমস্যা Workers নয়; সমস্যা হলো server-style PDF stack-কে isolate runtime-এ বসানো।

Workers Lambda নয়, তাই diagnosis আলাদা

Cloudflare Workers serverless container নয়। এগুলো V8 isolates, যার সীমা স্পষ্ট:

  • CPU time limit: Free plan-এ request প্রতি 50 ms, Workers Paid Bundled-এ 30 seconds, Unbound-এ 5 minutes. Wall time দীর্ঘ হতে পারে, কিন্তু billable।
  • Memory cap: isolate প্রতি 128 MB.
  • Bundle size: Free plan-এ 1 MB, paid-এ 10 MB.
  • Filesystem নেই। fs.readFileSync নেই; সব memory বা fetch।
  • Native binary নেই। Pure JavaScript / WebAssembly only; node-canvas, native zlib, Ghostscript shell-out নেই।
  • Cold start প্রায় 5 ms। এত দ্রুত কারণ boot করার মতো ভারী কিছু নেই।

“Workers-এ PDF slow” সমস্যার বেশিরভাগই এই constraint ভাঙে, সাধারণত CPU বা bundle size।

সত্যিকারের ধীর করার পাঁচ কারণ

1. Chromium renderer Workers-এ টানা

এটা চলে না। Puppeteer-এর প্রায় 250 MB Chromium এবং real OS দরকার। Cloudflare Browser Rendering API বা Browserless কাজ করে, কিন্তু সেগুলো Workers নয়; Worker শুধু remote service call করে, যেখানে প্রায় 500 ms round-trip + render time লাগে।

আপনার “Worker PDF” যদি আসলে “Worker remote browser API call করছে” হয়, latency floor প্রায় 500 ms। এটা Worker সমস্যা নয়; browser tax।

Diagnosis: code-এ fetch("https://browser-rendering.cloudflare.com/...") বা মিল আছে কি দেখুন। থাকলে আপনি upstream latency মাপছেন।

2. JavaScript-এ layout করা

নিজের JS layout engine লিখে box position আর text wrapping করলে CPU limit-এ ধাক্কা লাগবে। JS দ্রুত, কিন্তু 30+ element ও wrapping নিয়ে layout Workers Free-এ সহজেই 50 ms ছাড়ায়, Bundled-এ 100-300 ms হতে পারে।

এই pipeline ব্যয়বহুল:

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

একই data-র ওপর চারটি CPU-bound pass, সব JS-এ, সবখানে GC pressure এবং intermediate tree allocation।

Diagnosis: একটি render-এর wrangler tail দেখুন। I/O শুরু হওয়ার আগে CPU 50 ms ছাড়ালে bottleneck compute।

3. প্রতিটি request-এ font load করা

Font সাধারণত 50-250 KB। renderer যদি প্রতিটি render-এ KV / R2 থেকে font পড়ে, প্রতিটি font-এর জন্য network round-trip লাগে। পাঁচটি font মানে render শুরুর আগেই 50-150 ms।

Diagnosis: font-loading code-এ timing দিন। p50 সেখানে গেলে সমস্যা এখানেই।

Fix: Worker file-এর top level-এ module init সময় font একবার load করুন, request handler-এর ভেতরে নয়। isolate জীবিত থাকা পর্যন্ত bytes cache থাকে।

// 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 যদি font bytes inline করে, আরও ভালো: কোনো I/O নেই।

4. Workers-এর জন্য না বানানো JS PDF library ব্যবহার

pdfkit, pdf-lib, jsPDF Workers-এ চলে, কিন্তু cost আছে:

  • pdfkit Node Buffer shims চায়। সম্ভব, কিন্তু প্রায় 500 KB যোগ করে এবং compute প্রায় 30% ধীর করে।
  • pdf-lib existing PDFs edit করার জন্য ভালো; scratch থেকে emit করলে abstraction প্রতি page-এ প্রায় 10 ms overhead যোগ করতে পারে।
  • jsPDF browser-first; Buffer issue আছে এবং API surface বড়, tree-shake করা কঠিন।

যদি pipeline মূলত “JSON পড়ে PDF bytes লেখা” হয়, সরাসরি PDF emit করা purpose-built engine সাধারণত 5-20 গুণ দ্রুত। Rust বা C++ থেকে WebAssembly tight loop-এ ভালো কাজ করে।

5. Bundle গোপনে 4 MB হয়ে যাওয়া

Workers Free bundle limit 1 MB, Workers Bundled 10 MB. অনেক team wrangler deploy-এর “Script exceeds size limit” error-এ প্রথম বুঝতে পারে। কেউ কেউ আগেই cold start slow দেখে, কারণ V8-কে অনেক code compile করতে হয়।

Deploy output-এ wrangler bundle size দেখায়। 500 KB-এর বেশি হলে দেখুন:

  • Bundled fonts. Workers Assets-এ রাখুন এবং module init-এ একবার fetch করুন।
  • node: shim layer. Source map-এ __cf_KV বা polyfills: দেখলে bundler অপ্রয়োজনীয় Node APIs shim করছে।
  • Unused dependencies. Wrangler 4+ এ npm run build -- --analyze treemap দেয়।

Workers-এ দ্রুত PDF দেখতে কেমন

Structured documents-এর জন্য 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 remote Puppeteer/browser rendering call করে” path: p50 500-1000 ms, অন্য service-এ 1-2 GB browser memory, upstream cost প্রায় $0.001/render

দ্রুত triage

এখনই slow PDF হলে আগে এই checklist চালান:

  1. সময় কোথায় যাচ্ছে? wrangler tail-এ timestamps দিন। CPU, upstream fetch, cold start আলাদা করুন।
  2. JS layout চলছে? হলে সেটাই CPU-এর বড় অংশ। same-isolate renderer ব্যবহার করুন।
  3. Font প্রতি request load হচ্ছে? module init-এ সরান।
  4. External browser call করছেন? latency floor সেই service. same-isolate rendering-এ যান।
  5. Bundle 1 MB-এর বেশি? cold start bundle size-এর সঙ্গে বাড়ে। unused deps কাটুন।

Workers-এ দ্রুততম PDF হলো যেখানে document data থেকে PDF bytes তৈরি হয় এক fetch handler call-এর মধ্যে, কোনো internal fetch() নেই, heavy JS layout নেই।

সংক্ষিপ্ত কথা

Cloudflare Workers single-digit milliseconds-এ PDF render করতে পারে, যদি renderer isolate-shaped runtime-এর জন্য বানানো হয়। Node-ভিত্তিক JS PDF libs, remote browser services, per-request font loading এবং JavaScript layout p50-কে origin-এর থেকেও ধীর করতে পারে।

নিজে এই engineering করতে না চাইলে gPdf Playground দেখুন। এটি একই ধরনের edge runtime-এ চলে। Render PDF চাপুন, Network tab দেখুন; pipeline runtime-এর সঙ্গে লড়াই না করলে Worker এই গতিই দিতে পারে।