公众号一发文章,9 张图裂了 5 张。网页端、预览都正常,唯独微信编辑器里大面积裂图。如果你也用 Cloudflare R2 做图床,大概率会先怀疑水印、跨境、Worker CPU 超限——但真凶其实是 Worker 冷启动。
从 OSS 迁到 R2,是因为 OSS 出流量要钱,而 R2 零 egress 费(出站流量不收费)。OSS 有个很香的能力:同一份源图,URL 带参数就触发水印,不带就是原图。R2 是纯对象存储,没有这种 URL 参数处理,于是你用 Worker + Images binding 复刻:原图在 R2 里干净,访问时由 Worker 实时加水印、缩放、转 WebP 再返回。听起来完美,直到公众号排版。
微信发文章时会把文中的外链图片抓回自己的服务器转存,抓取有超时限制。图片响应太慢,微信就放弃,显示裂图。你开始排查,踩了三个坑。
第一个坑:以为是 Worker CPU 超限。Cloudflare Workers Free 套餐有 10ms CPU 上限,超限报 1102。你用 curl 打了一次冷请求,TTFB(首字节时间,从发出请求到收到第一个字节)7.9 秒,足以让微信超时。但再测一次只要 1.36 秒,且返回 200 + image/webp。说明 7.9 秒是冷请求,不是稳定慢。
第二个坑:以为是水印/transform 太慢。去后台翻日志,那次请求 cpuTimeMs 只有 8,远低于 10ms 上限;wallTimeMs 670,暖请求实际只跑 670ms。水印和缩放非常便宜,不是元凶。
第三个坑:真相是 Worker 冷启动。日志里的 colo: SEA 暴露了真相——第一次请求撞上一个从未服务过该请求的 Cloudflare 节点,Worker 运行时要从零拉起,Images 绑定要首次初始化,冷启动能到数秒。冷启动之所以慢,是因为 Worker 的 V8 隔离、绑定资源都要在请求到达时现场创建;Free 套餐没有常驻实例,冷启动长尾能到数秒。第二次落到已暖的节点,670ms 就搞定。微信爬虫从它自己的节点冷抓,撞上 7.9s 冷启动,超时裂图

。而你的浏览器访问博客时,你已经先访问预热了边缘缓存,所以网页正常。“裂 5 显 4”也吻合:4 张恰好命中某处已暖缓存踩线通过,5 张大图全是冷请求被掐。
还有一个被排除的嫌疑:纯跨境慢。R2 节点在 LAX/SEA,国内访问确实要跨境,但日志证明暖请求只要 ~1.3s,微信完全抓得到。跨境只是叠加项,不是裂图主因。
为了坐实,你新建了一个测试桶 + 新子域,直接绑 R2 原生直出(不经过任何 Worker),把同样的图重传公众号——一张都没裂。这彻底锁定:只要热路径上没有 Worker,公众号就不裂。
但光删 Worker 水印也没了。你想要的是一份源图,两个场景不同表现:公众号无水印不裂,博客带水印防搬运。最终方案是子域分流,几乎 1:1 复刻 OSS 的体验:
- 公众号链接 img.macin.org/blog/xxx.webp → R2 原生直出,无水印、零裂图
- 博客链接 wm.img.macin.org/blog/xxx.webp → Worker 实时加水印
关键认知:Worker 只服务 wm. 这一个子域,公众号用的 img. 完全不碰 Worker,永远不会撞冷启动。配置上,先在生产桶去掉 Worker 域名,把 img.macin.org 作为 Custom Domain 直接连到 R2;再给 wm.img.macin.org 单独绑 Worker,别用 /* 通配。上传流程不变,PicGo 只传一次到 R2,两个子域共享同一份源图。
水印逻辑也做了改进:按输出图面积的 5% 计算水印大小,固定右下角,不再写死像素。代码里用 Images binding 的 transform 和 draw 接口,输出 WebP 质量 82,并加了 stale-while-revalidate 缓存策略。
这件事的启发很直接:跨境慢不是裂图主因,偶发的 Worker 冷启动才是。任何需要微信抓取的外链图,都该走纯静态直出;如果非要水印,可以用微信自带的水印,或者像这样用子域分流。R2 做图床降本很好用,零 egress 真香,但把实时图片处理架在公众号抓取链路上,冷启动就是隐形炸弹。如果你也在用 R2 + Workers 做图床、被公众号裂图折磨,这个排查思路能直接省下你几小时。
内容与图片版权归原作者所有 · 原文: https://macin.org/2026/08/19/r2-image-wechat/