Nếu hôm nay bạn tìm “Puppeteer PDF alternative” và đến trang này, câu hỏi thật sự thường là một biến thể của:
“Vì sao serverless function của tôi cold-start mất 2 giây và dùng 900 MB RAM chỉ để in một invoice?”
Puppeteer là một công cụ xuất sắc. Nó cũng quá nặng cho công việc mà nhiều đội dùng nó để làm: biến dữ liệu có cấu trúc thành một PDF có thể dự đoán. Bài này dành cho đội sắp đưa Puppeteer vào production và đang tự hỏi liệu có lựa chọn hợp lý hơn không.
Chúng ta sẽ đi qua nơi Puppeteer xứng đáng với trọng lượng của nó, nơi nó không xứng đáng, và ma trận đánh đổi thực tế trong năm 2026.
Bạn thật sự đưa gì vào production cùng Puppeteer
Khi chạy npm install puppeteer, bạn kéo về khoảng 170 MB Chromium build trước cả transitive dependencies. Lúc chạy, headless Chromium cần 600-900 MB resident memory cho một lần render một trang, và 1-2 giây cold-start để khởi động browser. Mỗi lần render phải:
- Boot browser process, hoặc reuse một pool
- Mở tab mới
- Navigate tới HTML hoặc URL của bạn
- Chờ
domcontentloaded, và thường chờ thêm font, image, web component - Chạy
page.pdf(), serialize trang đã paint qua PDF engine của Chromium - Đóng tab
Đây là khoản thuế của toàn bộ web platform. Bạn trả nó dù tài liệu là hợp đồng pháp lý 90 trang có SVG chart nhúng, hay một nhãn vận chuyển một trang với năm dòng chữ.
Với use case HTML-to-PDF nơi input thật sự cần CSS layout, nội dung do JavaScript tạo, web fonts và phần còn lại của web platform, khoản thuế đó hợp lý. Với mọi thứ khác như hóa đơn, nhãn, biên nhận, vé, sao kê, chứng chỉ, nó là đốt tiền.
Khi nào Puppeteer thắng
Hãy trung thực về phần này trước, nếu không đội của bạn sẽ nghi ngờ quyết định về sau:
- Render HTML/CSS trung thành. Nếu design system của bạn xuất HTML và bạn muốn PDF giống pixel với HTML đó, Puppeteer gần như không có đối thủ. Nó chính là Chrome đang in.
- Tính năng web platform. SVG có filter, edge case CSS Grid, web component, nội dung được JavaScript tính toán, iframe bên thứ ba: tất cả đều hoạt động.
- Debug trực quan. Bạn có thể chụp screenshot giữa render, mở DevTools với headless mode và nhìn đúng thứ renderer nhìn thấy.
- Không có bước dịch schema. Nếu nội dung đã là webpage, không cần mapping.
page.goto(url); await page.pdf()là toàn bộ pipeline.
Nếu hai bullet trong số này mô tả workload thật của bạn, đừng chuyển. Puppeteer là câu trả lời đúng.
Khi nào Puppeteer thua nặng
Với phần còn lại, stack chi phí cộng dồn rất nhanh.
Memory và cold-start trong serverless
Một Node 20 Lambda hoặc Cloudflare Container điển hình chạy Puppeteer:
| Chỉ số | Giá trị thường thấy |
|---|---|
| Kích thước container image | 250-400 MB, gồm Chromium + Node + code của bạn |
| Cold-start time | 1,8-2,5 giây |
| Warm RAM mỗi lần render | 600-900 MB |
| Concurrent render trên mỗi instance 1 GB | 1, đôi khi 2 nếu page rất nhỏ |
Nếu invoice service xử lý 100.000 render/tháng, bạn đang trả chi phí boot browser cho mỗi cold container, dù không render nào trong số đó cần chạy JavaScript.
Bẫy “font trong container”
Chromium đi kèm một bộ font mặc định, thường thiếu CJK, Cyrillic, Devanagari, Arabic và nhiều glyph theo hệ chữ cụ thể. Phát hiện điều này trong production thường trông như sau:
Invoice Q3 2025 cho văn phòng Tokyo in ra
▢▢▢▢ 2025年第3四半期. Khách hàng escalation. Đội của bạn mất một sprint debug font install trong Dockerfile và CSS fallback font.
Chỉ riêng NotoSans CJK đã thêm khoảng 50 MB vào image. Embed một bộ Noto fallback toàn cầu thêm khoảng 250 MB. Bạn đang trả tiền cho Chromium và một nhà thờ font, chỉ để in một invoice tiếng Nhật.
Determinism
Render từ Puppeteer không byte-identical giữa các version Chromium. Một patch upgrade có thể dịch nhẹ kerning, baseline font hoặc vị trí page break. Nếu test suite của bạn diff PDF, và nên có, mỗi lần cập nhật Chromium là một cuộc khai quật nhỏ: render nào đổi, đổi đó có chủ ý không?
JavaScript tại thời điểm render
Ngay cả trang HTML “static” của bạn cũng phải được parse, layout, paint và serialize. Thực nghiệm thường là 80-400 ms mỗi trang trên warm process. Phần lớn là layout, không phải paint.
Để so sánh: một server trả JSON trực tiếp cho binary renderer mất 3-8 ms cho cùng invoice một trang. Phần dưới sẽ nói rõ con số này.
gPdf phù hợp ở đâu
gPdf đảo mô hình: thay vì mô tả tài liệu bằng HTML rồi nhờ browser paint, bạn mô tả bằng JSON có cấu trúc (DocumentRequest) và một renderer Rust compile sang WebAssembly emit PDF trực tiếp. Không browser. Không DOM. Không JavaScript layout pass.
Điều đó nghe có vẻ hạn chế, và đúng là hạn chế với bài toán có hình dạng HTML. Nhưng với nhóm tài liệu invoice / label / receipt / statement / certificate, mô hình JSON-first lại hợp hơn:
- Dữ liệu của bạn vốn đã có cấu trúc. Invoice của bạn đã tồn tại đâu đó dưới dạng
{ customer, lines, totals, taxes, notes }. Bạn không muốn render nó thành HTML trước, rồi bắt browser đọc HTML ngược lại thành layout. Bạn muốn đi thẳng từ data sang PDF. - Layout trở thành contract. Khi
font_size: 11luôn là 11 point vàgap: 8luôn là 8 point, hai engineer review cùng một PR sẽ thấy cùng output. Không có khoảng mờ do cách diễn giảidisplay: flex. - Output byte-identical. Cùng input tạo cùng bytes. Bạn có thể
git diffhai PDF và chỉ thấy thứ thật sự đổi. - Cold start là startup runtime, không phải boot browser. V8 isolate trên Cloudflare Workers khởi tạo trong 5-20 ms. WASM module được giữ nóng trong memory qua nhiều invocation trên cùng isolate.
Một lần render invoice một trang bằng gPdf thường đạt 3-5 ms p50 wall-clock tại Edge, phục vụ từ Cloudflare colo mà người dùng đi vào. Con số này nhanh hơn Puppeteer warm path khoảng hai bậc độ lớn, và nhanh hơn cold path của Puppeteer khoảng ba bậc độ lớn.
Ma trận quyết định
Đây là bảng bạn thật sự dùng trong tech-design review.
| Workload | Dùng Puppeteer | Dùng gPdf |
|---|---|---|
| Báo cáo HTML hiện có -> PDF | lựa chọn đầu tiên | cần viết lại |
| Hóa đơn, sao kê, biên nhận | quá nặng | lựa chọn đầu tiên |
| Nhãn vận chuyển có barcode | nên tránh, rủi ro font | lựa chọn đầu tiên |
| E-invoice (Factur-X / ZUGFeRD / EN 16931) | không built-in | built-in |
| Lưu trữ dài hạn PDF/A | cần thêm pass Ghostscript | profile built-in |
| Mockup design system cần giống pixel | lựa chọn đầu tiên | sai công cụ |
| Chart cần D3 / Recharts thật | lựa chọn đầu tiên | sai công cụ |
| Vé, chứng chỉ, name tag | overkill | lựa chọn đầu tiên |
| Bất kỳ thứ gì cần JavaScript lúc render | lựa chọn duy nhất | sai công cụ |
Nếu bạn nằm ở cột phải trên hơn ba hàng, khoản tiết kiệm sẽ không nhỏ.
So sánh thực tế: render invoice một trang
Cùng nội dung. Cùng khổ giấy. Cùng font (NotoSans). Cùng profile output PDF/A-3b.
| Puppeteer (warm Lambda, 1 GB) | gPdf (warm Cloudflare Worker) | |
|---|---|---|
| p50 latency | 180 ms | 3,4 ms |
| p99 latency | 420 ms | 8 ms |
| Cold-start penalty | +1800 ms cho render đầu | +12 ms cho render đầu |
| Memory ở peak | 720 MB | 18 MB |
| Image / module size | 280 MB | 4,5 MB |
| CJK glyphs | không, trừ khi cài rõ ràng | có, embed NotoSans CJK |
| Chi phí / 100.000 render | khoảng 240 USD, Lambda compute | khoảng 5 USD, gPdf Basic plan |
Hàng cuối thường làm mọi người bất ngờ. Khoảng cách chi phí là thật và không phải giá mồi. Nó mang tính cấu trúc. Chúng tôi không phải phân bổ chi phí boot Chromium, memory browser hay cold-start container, nên unit cost mỗi lần render thật sự rất nhỏ.
“Nhưng 5 USD/100.000 trang nghe rẻ quá. Có bẫy gì không?”
Bẫy chính là chúng tôi không đưa browser vào pipeline. Chi phí chạy một binary renderer trên warm V8 isolate là vài millisecond CPU và kilobyte memory. Tính giá kiểu Puppeteer cho nó sẽ là tính tiền cho hạ tầng mà chúng tôi không vận hành.
Khi nào vẫn nên chọn Puppeteer
Nếu câu trả lời của chúng tôi luôn là “dùng gPdf”, chúng tôi sẽ là người tệ nhất để hỏi. Không phải vậy. Các trường hợp trung thực:
-
Bạn đã chạy Puppeteer trong production và nó hoạt động ổn. Đừng migrate chỉ để migrate. Thời điểm đúng để đánh giá gPdf là khi Puppeteer bắt đầu gây đau: thường là khi hóa đơn compute hàng tháng vượt 400 USD, hoặc cold-start SLA làm hỏng một phần downstream.
-
Tài liệu của bạn là webpage hiện có, chấm hết. Một báo cáo 60 trang do người dùng tạo, được style bởi design system, có chart lồng nhau và nội dung dynamic, không phải migration JSON. Đó là redesign.
-
Bạn cần parity pixel-perfect với web preview. Một số workflow, ví dụ “thấy gì trong editor thì in ra đúng vậy”, thật sự cần Chromium làm renderer ở cả hai nơi.
Nếu không có trường hợp nào áp dụng, phép tính khá thẳng: deploy nhỏ hơn, latency thấp hơn, hóa đơn thấp hơn, output byte-identical và không còn drama cài font.
Cách migrate một workload thật
Nếu bạn đủ thuyết phục để thử, migration thường là spike 1-2 ngày cho mỗi loại tài liệu, không phải tái kiến trúc:
- Chọn một tài liệu, bắt đầu với loại volume cao nhất chứ không phải loại phức tạp nhất.
- Map các phần logic của HTML template sang element JSON của gPdf:
text,box,table,barcode,image. - Dùng Playground để lặp trên một DocumentRequest thật cho đến khi output khớp.
- Nối data-shape hiện có của bạn vào một mapper nhỏ emit JSON.
- A/B endpoint mới với endpoint Puppeteer trong một tuần. Diff PDF. Quyết định.
Phần lớn đội thấy mô hình JSON “click” trong một ngày. Phần khó không phải công cụ mới, mà là gỡ đống HTML/CSS workaround mà template cũ tích lũy theo thời gian.
TL;DR
Puppeteer là câu trả lời đúng cho web page. Với tài liệu, bạn đang trả khoản thuế 100-200 lần ở mỗi lần render để tránh một chi phí nhỏ ban đầu: mô tả tài liệu như data. Nếu fleet của bạn render hóa đơn, nhãn, biên nhận, sao kê, vé hoặc bất kỳ thứ gì “cùng hình dạng, chỉ khác giá trị”, một renderer edge-native như gPdf sẽ nhanh hơn, nhỏ hơn, rẻ hơn và deterministic hơn một cách đo được.
Thử trong Playground: đó là edge worker thật, không cần signup, phản hồi trong browser dưới 5 ms.