从 3 MiB 到 64 MiB:重看 Fluxship 的 Cloudflare 部署方案

从 3 MiB 到 64 MiB:重看 Fluxship 的 Cloudflare 部署方案

沿着 Fluxship 的 11 个部署提交,复盘 OpenNext、vinext、依赖拆分与运行时修复,再看 Cloudflare 放宽到 64 MiB 后,Node 与 Next.js 项目该怎样部署和验收。

—次点击16分钟阅读

Cloudflare 放宽 Worker 包体积限制以后,我又翻了一遍 Fluxship 的部署记录。六月为了把站点放进 Workers,改过构建入口,拆过编辑器,挪过埋点,也为几个线上才暴露的运行时问题补过代码。当时看起来是一次“换个地方部署”,最后却把应用的依赖关系重新梳理了一遍。

现在的变化很值得写,但如果只说“以前 3 MB,现在 64 MB,Node 项目可以随便上了”,会漏掉最有用的部分。包能上传、框架能运行、业务能正常使用,分别需要不同的证据。这篇就沿着 Fluxship 的 PR,把六月的做法与九月的新条件放在一起看。

本文写于 2026 年 9 月 24 日。项目部分以六月的部署 PR 和其中的提交为依据,平台规则以当天核对的官方文档为准。后面提出的新部署流程,是基于这些记录整理的建议,没有把它写成一次已经完成的新迁移。

先弄清楚,64 MiB 放宽的究竟是什么

Cloudflare 在 9 月 4 日的变更说明里明确取消了 gzip 压缩后的包体积限制。过去免费版按约 3 MiB、付费版按约 10 MiB 检查压缩包;现在两者统一按未压缩的 Worker bundle 检查,上限是 64 MiB。

所以这不是同一种口径下简单地从 3 扩到 64。原来 Wrangler 输出里最紧张的那一列是 gzip,现在需要对照上限的是 Total Upload。压缩率很高的代码、压缩率很低的数据,变化后的收益不会相同,也不适合直接算成“容量提升二十多倍”。

Worker 体积限制的口径变化:旧规则分别检查免费版 3 MiB、付费版 10 MiB 的 gzip 体积;新规则取消压缩体积门槛,统一检查 64 MiB 未压缩 bundle。
Worker 体积限制的口径变化:旧规则分别检查免费版 3 MiB、付费版 10 MiB 的 gzip 体积;新规则取消压缩体积门槛,统一检查 64 MiB 未压缩 bundle。

我会把几个经常混在一起的数字分开记:

左右滑动查看完整表格

检查项

当前含义

对部署的影响

Worker bundle

未压缩上限 64 MiB

检查实际上传的 Worker 代码及模块

gzip 体积

仍会输出,已不是平台包大小门槛

可以作为项目自己的观察指标

运行内存

每个 isolate 128 MB

大量缓冲、解析和并发仍可能耗尽内存

启动时间

顶层初始化需要在 1 秒内完成

包变大后,初始化工作仍需控制

静态资源

独立的文件数量与单文件限制

不应和 Worker bundle 当成一个包来计算

这些限制的当前口径见 Workers Limits。其中免费版 HTTP 请求的 CPU 时间仍是 10 ms;付费版默认 30 秒,可配置到 5 分钟。CPU 时间与等待网络的时间也不同。一个依赖很多、但大部分时间在等 API 的项目,与一个持续进行图片编码的项目,不能只看包大小判断适不适合 Workers。

这次放宽最直接的意义,是让不少项目不必再为了 gzip 多出几百 KiB,先牺牲产品功能。但架构是否合理,仍然要往下一层看。

OpenNext 解决的是 Next.js 适配,不只是体积

Fluxship 是一个包含 Next.js Web 应用和独立 API 应用的 monorepo。Web 侧既有公开页面,也有多语言、文档、认证、后台和编辑器。它不只是几个静态 HTML,更不是随便换一个启动命令就结束的服务。

仓库里保留的 OpenNext 路径很清楚:先做 Next.js 构建,再由适配器转换产物,最后使用 Wrangler 预览或部署。项目当时的构建脚本拆开看,是下面两步:

bash
next build --webpack
opennextjs-cloudflare build \
  --skipNextBuild

这里的 --skipNextBuild 是因为前一步已经完成构建,不能脱离上下文理解成“OpenNext 不需要 Next.js 产物”。Webpack 也是当时仓库固定版本下的选择,不是要求所有今天的新项目照搬。

对应的 Wrangler 配置把入口指向 .open-next/worker.js,静态资源指向 .open-next/assets。open-next.config.ts 使用 defineCloudflareConfig;当时 R2 增量缓存相关配置仍是注释,不能因为文件里出现过名字,就认为缓存已经接通。

OpenNext Cloudflare 文档说明了这层适配的作用:将 Next.js 构建结果转换成 Workers 可以运行的形态,并使用 Next.js 的 Node.js runtime。它与过去要求 Edge runtime 的 next-on-pages 路线不同。Node.js API 兼容解决的是依赖能否调用某个 API;框架适配还要处理请求入口、路由和构建产物,两者不是一回事。

顺带说一个很容易踩的文档时间差:这次查看时,OpenNext 概览页末尾仍写着旧的 3 MiB / 10 MiB gzip 限制。平台配额应以 Cloudflare 最新说明为准,不能因为适配器文档的一段旧提示,就认定新规则没有生效。

找到真正的 PR,而不是只看最初的标题

这轮 Web 改造最初出现在 bunship-ai/fluxship 的 PR #7,创建于 6 月 5 日。随后它被关闭,讨论明确说明迁往个人 Fork,以便使用那边的 GitHub Actions 环境继续验证。

真正串起后续过程的是 virgoone/fluxship 的 PR #1。截至本次复盘,它仍处于打开状态,共包含 11 个提交,最后一个是 6 月 10 日的运行时修复。末次 Deploy Ship Web 检查为成功;这能够证明那次流水线完成,不能顺手写成“PR 已合并”或“所有业务已经验收”。

我把最值得回看的节点列在这里:

左右滑动查看完整表格

时间

提交

解决的具体问题

6 月 5 日

1a55e4c

增加 vinext 构建、Worker 入口和部署流水线

6 月 5 日

bf27105、064f45f

调整部署环境处理,增加 gzip 体积检查

6 月 8 日

7b04623

减少重型组件和渲染依赖进入产物

6 月 8 日

13010b5、c37ff43

埋点改走 CDN,编辑器改用远程组件

6 月 8 日

17fb21f、eb0f4e8、4c81fb3

数据改从 API 获取,拆清公开前缀和运行环境

6 月 10 日

6c571f5

修复数字组件、文档搜索和认证初始化问题

这些提交比 PR 最初的说明更可靠。例如说明里还保留着“按提交 SHA 创建预览 Worker”的描述,但后续代码已改成可配置的固定预览名称。读历史时,如果只读 PR 正文,很容易把初稿方案当成最终实现。

第二条路径:让 vinext 与原构建并存

六月的改造没有一上来删掉 OpenNext。它在现有脚本旁边增加 dev:vinext、build:vinext、dry-run:vinext 和 deploy:vinext,把新的部署链路留成一条可以独立验证的路径。

这件事的价值在于降低一次改造里的变量数量。原来的 Next.js 开发方式仍可参考,新工具链则单独检查。某个页面出错时,可以进一步判断是业务数据问题、Vite 构建差异,还是 Workers 环境差异,而不是把所有变化一次性揉在一起。

vite.config.ts 里加入了 MDX、vinext 和 Cloudflare 插件。Cloudflare 插件使用 wrangler.vinext.jsonc,并配置 RSC 与 SSR 构建环境。文档集合的 JSON 导入也增加了预处理;monorepo 里的编辑器配置和共享 CSS 则通过 alias 明确定位。

入口变为项目自己的 worker/index.ts。它先判断请求是否属于图片优化路径:本地资源通过 ASSETS 读取,图片转换交给 IMAGES binding;普通请求再进入 vinext 的 App Router handler。这解释了为什么“首页能显示图片”还不足以验收图片链路——原图直出和优化请求经过的路径并不相同。

Fluxship 的两条构建路径:OpenNext 转换 Next.js 产物,vinext 使用 Vite 构建并交给自定义 Worker;静态资源与 API 服务各自保留边界。
Fluxship 的两条构建路径:OpenNext 转换 Next.js 产物,vinext 使用 Vite 构建并交给自定义 Worker;静态资源与 API 服务各自保留边界。

部署时还有一个实际细节:vinext 路径使用的是生成后的 dist/server/wrangler.json。源配置负责告诉构建工具应该生成什么,最终配置才指向真实产物。把旧 OpenNext 入口、vinext 源配置和生成目录混用,可能得到一个看起来合法、却上传错对象的部署命令。

这轮 PR 的操作顺序可以压缩成:

bash
cd apps/ship
bun run build:vinext
cd dist/server

wrangler deploy \
  --config wrangler.json \
  --name fluxship-preview \
  --dry-run

这段只展示历史产物如何检查,不包含真实凭据,也没有执行正式部署。对于今天新建的项目,应使用当前版本生成的配置,而不是把六月的依赖版本和入口文件原样复制过去。

体积压力下,哪些拆分改变了产品

064f45f 加入的检查非常直接:解析 Wrangler dry-run 输出中的 gzip KiB,默认阈值是 3072,超出就中止。它还会列出体积较大的文件,帮助判断应该先看哪里。至少部署失败从“平台拒绝了”变成了在流水线里有明确位置的检查。

接下来的减重并不全是无感优化。7b04623 移除了动态 Open Graph 图片路由,减少组件预览和编辑器相关依赖,还把 Mermaid 渲染改成显示图表源码的代码块。它确实改变了依赖图,也改变了页面能提供什么。把这种取舍只写成“成功优化了包体积”,会把代价藏起来。

埋点经历了一个更完整的过程:先从应用内依赖链里减去重型实现,再由 13010b5 改成浏览器加载 CDN 脚本。这样可以减少构建耦合,但多出脚本加载失败、初始化时机和版本管理等问题。功能没有凭空消失,只是从一个部署产物转移到了另一条加载链路。

编辑器也是类似思路。c37ff43 引入远程 Web Component,主应用通过属性与事件同步内容,组件脚本在浏览器端加载;失败时保留 JSON 文本编辑兜底。它不是简单加一句动态 import,而是重新定义宿主应用和编辑器之间的数据协议。

我觉得这段最值得保留的经验,是先分清“用户现在不需要加载”与“Worker 永远不应该承担”。富文本编辑器不一定属于公开首页的服务端依赖;公共文章展示也不应该为了读内容,就引入整套可编辑状态和插件。即使没有 3 MiB 限制,这些边界仍然有价值。

但远程加载不是免费的午餐。宿主与组件版本不一致、CDN 不可达、内容协议变更,都要有人处理。64 MiB 放宽以后,我会重新评估当时为了过线而牺牲的功能,以及那些跨应用拆分带来的长期维护成本,不会机械地把所有拆分都继续加码。

历史 Actions 日志这次已无法下载,接口返回 410,因此本文没有给出“从多少 MB 降到多少 MB”的数字。提交能够证明改了什么,保留下来的检查结果能够证明最后一次部署任务成功,但它们不能补出已经缺失的测量值。

比删依赖更有用的,是把 Web 与 API 分清

Fluxship 还有一处改动比挪走某个大包更值得复用。提示词页面之前会从 Web 侧直接导入 API 应用里的服务函数。看起来省了一次 HTTP 调用,实际却可能把数据库、服务端配置和它们的间接依赖带进 Web 的构建图。

17fb21f 改为通过 API Worker 获取提示词,并设置三秒请求超时和后续兜底来源。Web 得到自己要展示的数据,不再通过导入服务实现来承担另一端的运行环境。

这不是说 monorepo 里不能共享代码。类型、数据校验和纯函数仍然适合共享;带着数据库连接、认证上下文和运行环境假设的服务入口,则需要更谨慎。目录放在同一个仓库,不代表所有包都应该进入同一个 Worker。

前面的初始提交也删除了 Web 侧临时的 catch-all API 路由。因此发布验收时,除了 Web 页面,还必须验证浏览器请求实际去了哪里。静态页面成功不说明 API 域名、路径前缀、Cookie 和跨域设置也都正确。

这一点随后真的变成了修复点。项目同时存在公开的 /api/v1 和服务内部的 /v1。最早根据 CF_WORKER 之类的环境标志推断路径,后来发现“代码跑在 Cloudflare”并不能说明“浏览器应该请求哪个前缀”。

eb0f4e8 增加可配置的公开 API 前缀,4c81fb3 又停止让公共地址解析跟着 Worker 环境标志走,并在 Web 构建、dry-run 与发布命令前清除几项可能串用的标志。这个修复解决的是环境语义:后端挂载在哪里,由后端决定;浏览器请求哪里,由公开配置明确表达。

如果今天重新部署,我仍会优先保留这条边界。更大的包只意味着可以塞进更多代码,不代表把 Web、数据库服务和任务执行器重新塞到一起,会让问题更容易维护。

构建过了,真正的页面还会在哪里出错

最后一个提交 6c571f5 很适合作为这篇文章的提醒。它处理的问题已经不是上传体积,而是应用在新运行环境里如何初始化。

数字动画组件改成先输出可读的静态数字,再在客户端 effect 里加载实际组件。这样服务端渲染不必为了一个数字动画提前执行浏览器相关逻辑,首屏也不会因为增强效果尚未就绪而没有内容。

文档搜索则补了另一类边界。不同构建结果里的结构化文档数据,可能直接存在,也可能通过函数或 load() 异步取得。修复后的代码先归一化这几种形式,再交给索引构造逻辑;搜索 handler 也延后初始化。一个普通文档页能打开,并不代表搜索索引读取的是同一种数据形态。

认证部分为 Clerk 的 publishable key 增加了明确处理。缺少配置时,公开路由仍能走国际化逻辑,受保护页面会重定向;存在配置时,再创建相应中间件。这是对异常配置路径的处理,不应被解释成缺少密钥也能完整登录。

这些例子说明,我不会用一次 dry-run 代替页面验收。至少要访问一个 SSR 页面、一个动态数据页面、一个登录相关页面、一个依赖浏览器组件的页面,再试一次图片优化和文档搜索。每条路径都代表一种不同的依赖与初始化方式。

九月再部署,我会怎样选方案

截至这次查阅,Cloudflare 的 Next.js 部署指南已经把 vinext 作为推荐入口,同时明确它仍处于 beta,已有生产项目需要先检查兼容性。它是基于 Vite 重新实现 Next.js API 的工具链,和 OpenNext 转换官方 Next.js 构建产物的方式不同。

所以我会按项目现状选,而不是按哪条命令更短选。如果现有 OpenNext 路径已经稳定,升级工具链、核查新限制和项目功能会比立刻换构建系统更直接。如果本来就准备调整构建,vinext 则值得在独立预览里评估;兼容性检查通过以后,仍要走刚才那些真实页面。

对于没有 Next.js 的 Node 风格 API,也没有必要先加 OpenNext。可以评估框架的 Workers 入口与依赖兼容性,把请求处理接到 Worker;若依赖外部进程、原生二进制或完整操作系统能力,再考虑 Cloudflare Containers或者现有 Node 容器部署。容器是另一类运行环境,不是 Worker 的 64 MiB 模式。

Node.js 兼容性本身也有新变化:2026 年 8 月 4 日起的 compatibility date默认启用 Node.js 兼容能力。六月的 Fluxship 配置仍显式声明 nodejs_compat,符合当时的条件;新项目不必把旧 flag 当成神秘配方。旧项目升级日期时,应结合 Wrangler 版本与回归结果一起做。

同时,API 支持表仍区分完整支持、部分支持和只能导入的 stub。尤其是需要创建子进程或工作线程的包,导入成功不等于方法可以执行。node:fs 提供的也是 Workers 的虚拟文件系统;需要长期保存的用户数据不能靠临时目录承担,具体语义见 文件系统文档。

对 Fluxship 这类应用,我现在更倾向于维持清楚的分工:Web Worker 负责页面与必要的服务端渲染,API 负责业务规则与数据访问,长任务交给已有的队列执行链路,静态资源走资源服务。这是根据项目改造得到的选择,不是所有 Node 项目都必须复制的结构。

新限制下,CI 也要跟着换判断方式

有一个很现实的问题:平台已经放宽,自己的流水线可能还在挡路。Fluxship 六月版本里的 SHIP_WORKER_MAX_GZIP_KIB 默认值是 3072。只升级 Cloudflare 或依赖版本,而不调整这个检查,构建仍然会被自己的旧规则中止。

我的调整顺序会是先保留 dry-run,再把硬门槛改成未压缩体积,将 gzip 留作趋势记录。不能简单把旧 gzip 变量的值改成 65536,因为这仍然在比较错误的一列。记录里最好同时带上提交 SHA、构建工具版本和当前兼容日期,否则过几个月又会不知道两个数字是不是同一种产物。

下面是给新流水线用的检查思路,属于方案示意,并非已经提交到 Fluxship 的改动:

代码
构建固定版本的应用
  → 读取生成的 Worker 配置
  → Wrangler dry-run
  → 检查未压缩包体积
  → 发布独立预览
  → 验证真实页面与 API
  → 再切换正式入口
部署验收顺序:锁定构建条件,检查真实产物和未压缩体积,进入预览环境验证页面与业务,最后再发布正式版本。
部署验收顺序:锁定构建条件,检查真实产物和未压缩体积,进入预览环境验证页面与业务,最后再发布正式版本。

环境配置也需要重新审视。六月 PR 后续版本使用固定预览 Worker,并从名为 Production 的 GitHub Environment 读取配置。这里能确认的是历史代码的做法,不能由“预览地址不同”推导出数据库、支付和存储也已隔离。新方案里我会明确预览资源和允许的写操作,并按 Worker 实际职责注入所需配置。

部署重试同样应该有条件。网络中断适合有限重试;同一个 bundle 超出限制、配置字段错误或者绑定不存在,重跑五次不会变正确。先给错误分类,比把重试次数加大更有帮助。

最终验收记录也应分开保存:构建成功、包体积符合规则、预览发布成功、用户路径通过,分别留下结果。这样即使以后日志过期,至少还能知道当时验证到哪一步,而不是只剩一个绿色图标。

我想从这次变化里带走什么

64 MiB 对我最有吸引力的地方,是可以把注意力从“再挤掉一点 gzip”转回应用本身。某些为了过线临时移除的展示能力,可以重新评估;某些引入跨站依赖的拆分,也可以重新比较收益与成本。

但 Fluxship 那轮改造里真正留下来的东西,不只是一条可以部署的命令。公开 API 前缀与服务挂载前缀分开了,Web 不再为了取一份数据直接导入另一端的服务,浏览器组件和服务端初始化也有了更明确的边界。这些变化不会因为包上限增加而失去意义。

如果再给一个 Node 或 Next.js 项目设计 Cloudflare 方案,我会先画出请求、构建产物和依赖分别在哪里,再选择 OpenNext、vinext 或容器路径。体积只是其中一道关口。能够解释每一层为什么放在那里,后面加功能、换部署方式和排查故障时,才有真正可用的依据。

想接着看任务执行部分,可以读 Bunship 的生成任务与多 Provider 设计;它讨论的是进入队列以后如何执行与恢复,和这一篇的 Web 构建与发布边界正好衔接。