生成一张发票PDF,真的需要搬进整个浏览器?

大多数文档生成流水线都从同一个起点开始:HTML模板进来,无头浏览器跑一会儿,PDF出去。这个默认选型的逻辑很直白——浏览器是顶级的CSS渲染器,没有哪个工程团队愿意为了打印几张发票,专门维护一套自己的排版引擎。

但原型验证阶段被顺手选中的默认方案,到了生产环境往往会变成沉重的包袱。这也是我评估 Bhowra.Ink 这个 .NET 库时想搞清楚的问题:对于静态商务文档,我们真的需要为了一次 PDF 生成就把一个完整的浏览器塞进运行时里吗?

如果把无头浏览器单纯当打印引擎用,成本会从四个方向浮现出来。

1. 部署体积。 一条依赖浏览器的 PDF 管线通常会连带拉进数百兆的浏览器二进制文件。这不只是看起来臃肿,它会直接拖慢构建时间、胀大容器镜像体积、占用注册表存储空间、延长发布速度,并且让冷启动和漏洞扫描都变得更重。对于一个任务只是渲染静态发票 HTML 的服务来说,这些机械装置未免太大了。

2. 延迟。 浏览器进程必须启动之后,PDF 的第一个字节才有可能出现。在长时间运行的服务器上这点开销也许能被接受,但在 serverless 平台上,冷启动本身就是运营模型的一部分。如果 PDF 生成要跑在 Azure Functions、AWS Lambda 或短生命周期容器里,进程启动耗时就是一个绕不开的变量。

3. 内存。 浏览器被设计来渲染整个网页,这意味着 JavaScript 执行、布局计算、绘制、网络请求、字体加载、图片解码、安全隔离以及各种浏览器 API 一个都不会少。渲染一个真正的 Web 页面时,这才是你想要的;但面对静态的发票、对账单、工资单和报表,这些全都是非必要的额外开销。

4. 安全责任归属。 只要 Chromium 进入了你的生产管道,它就变成了你需要承担运维责任的组件。浏览器的 CVE、补丁节奏、依赖扫描、沙箱行为表现、容器加固和运行时兼容性,都会成为你自己的事情。对某些场景来说,这样的取舍完全值得。可对于静态商务文档,我更想弄明白一个更轻的渲染栈是不是已经足够。

这里要先说清楚一点:并不是要否定基于浏览器的渲染这条技术路线本身。如果你的文件需要 JavaScript 执行、SPA 渲染、浏览器 API、canvas 输出,或者依赖客户端图表库、要求对真实网页进行截图级的还原,那请尽管用 Playwright、Puppeteer 或者 Chromium。这正是浏览器最擅长的事。

然而大量的企业级 PDF 实质上并不是 Web 应用,它们只是服务器端渲染的静态文档:发票、收据、对账单、工资单、报表、合同、证书、标签。为了这些文档跑一整套浏览器,很可能属于过度设计。

我考察的替代方案,就是用起来就像普通 .NET 库一样的东西。一句 dotnet add package Bhowra.Ink 就能引入,紧接着就是几行代码:把 HTML 传给库,设置页眉页脚、页边距和密码保护,然后直接输出 PDF 字节流。整个流程没有浏览器进程,没有额外的运行时依赖,部署体积从数百兆陡降到 1MB 左右。

这部分体量的变化,最直接的受益场景就是 Azure Functions 这类 serverless 环境。一个函数实例冷启动时不再需要解压并预热一个浏览器上下文,内存基线随之降低,包的扫描面也大幅度缩小。对于每小时只需要生成几十份对账单的系统来说,不再有“为了偶尔打一张纸,常年养着一头大象”的荒谬感。

当然,轻量化并非没有边界。Bhowra.Ink 目前并不支持 JavaScript,也不处理依赖客户端状态的 SPA,而是专注在服务端已经组装好的 HTML 和 CSS 子集上。它的布局能力涵盖分页、表格、浮动、flex 布局和基础排版属性,已经能够覆盖绝大多数静态商务文档的格式化需求。当文档结构并不复杂、且源 HTML 完全在掌控之内时,这个取舍在工程账面上是划算的。

从技术评估的角度来看,把“PDF 生成”从“需要浏览器”重新定义为“需要一份可控的排版实现”,这个视角转换本身就减少了大量的运维风险和资源浪费。对于运行在 Azure Functions 上的 .NET 项目,这一替换意味着冷启动更快、部署包更小、安全审计更简单,也让“生成一份 PDF”这件事,回归到它原本应该有的重量。