Bun Image 实战:4 步从 Sharp 迁移到 Bun 1.3.14
Bun 1.3.14 内置 Bun Image,无需原生插件即可处理 JPEG、PNG、WebP、AVIF 和 HEIC。本文用生产代码拆解从 Sharp 迁移的 4 个步骤,实测 CI 安装耗时、Docker 镜像体积与单图性能,并说明 ICC、动图帧、tile 和任意角度旋转等场景为何仍需保留 Sharp。

Bun 于 2026 年 5 月 13 日发布 v1.3.14,其中带来了 Bun Image:一套基于 libjpeg-turbo + spng + libwebp、兼容 Sharp API 的图像处理管线,全程不需要任何原生插件构建步骤。过去三年,每次升级 Node,CI 都会被 lovell/sharp 的 libvips 二进制文件拖垮;正是这个版本,让我最终把 Sharp 移出了项目。
为什么 Bun 1.3.14 的 Bun Image 让我当天就移除了 Sharp
Bun 1.3.14 的发布说明于 5 月 13 日上线。和往常一样,更新内容列成了一长串;藏在“HTTP/3 客户端”和“热安装提速 7 倍”下面的一行,却终结了我继续使用 Sharp 的日子:Bun.Image。这是一套直接内置于运行时、支持链式调用的图像处理管线,libjpeg-turbo、spng 和 libwebp 都被直接编译进 Bun 二进制文件。
如果你从没因为 Sharp 浪费过整整一天 CI 时间,这一段可以跳过。经历过的人都熟悉这套剧本:找不到 sharp/lib/sharp-linuxmusl-x64.node;Alpine 容器里报 Cannot find module '../build/Release/sharp.node';有人升级了 Node,Docker 层缓存随即失效,此后每次推送都要从头执行 npm rebuild sharp;Vercel 构建访问预编译二进制 CDN,最后超时。三个项目、三年反复踩坑,到后来我几乎凭肌肉记忆就会敲出 apk add --no-cache vips-dev。
Bun.Image 给出的是运行时原生方案。图像编解码器随 Bun 二进制文件一同交付,图像处理不再需要 npm install 步骤。Node ABI 变化时也没有原生插件需要重建,因为这里既没有 Node,也没有插件。几何运算内核采用 i16 定点 SIMD;解码 JPEG 时,还会自动缩放到满足需求的最小尺寸。从架构上看,它就像一个无需受限于 Node 插件形态的 Sharp。
1.3.14 对我还有另一层意义:这是 Anthropic 资助的 Rust 重写落地前,最后一个使用 Zig 的版本。The Register 在 5 月 14 日报道了项目合并节奏,接下来的工程推进速度会相当快。与其让原生插件依赖跟着运行时一起经历重写,我更愿意现在就迁移到 Bun 的原生能力。
这并不是 Sharp 的讣告。基于 libvips 的 Sharp 在动图 WebP、对色彩配置文件有严格要求的摄影工作流,以及 tile() 深度缩放金字塔这三个场景中,仍然是性能王者。下面这套迁移方案面向其余 95% 的图像任务,也就是大多数生产应用实际运行的“解码—缩放—编码”管线。
封面图管线迁移到 Bun Image 的 4 个步骤
我在 omidsaffari-admin 上完成了这次替换。这个 worker 会在 PublishWorkflow 的 cover 步骤之后处理 gpt-image-2 生成的封面,再将结果写入 R2。一个文件里共有 8 处 Sharp 调用,全部位于这条处理路径中。包括 WebP 与 JPEG 回退的双编码逻辑在内,整个迁移用了 42 分钟。
第 1 步——盘点 Sharp 的使用范围。 动手之前,先找出所有 import:
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/需要先确认这次替换涉及 4 处调用还是 40 处。如果是 40 处,就按路由逐个迁移,不要试图一次完成。
第 2 步——用 Bun.file().image() 替换 import。 Sharp 的构造函数可以接收路径、Buffer 或 Stream。Bun.Image 的构造函数则可通过 Bun.file() 接收路径,也能接收 Uint8Array、Blob,以及 Bun 文件原语返回的任何对象,其中也包括 Bun.s3() 引用;这让我的代码结构也随之改变。
第 3 步——对应链式 API。 这正是 Bun.Image 被称为“兼容 Sharp”的底气。我在线上使用的方法都能 1:1 对应:.resize(w, h, { fit: "cover" }) 完全一致;.rotate(90) 可用,但要留意下文的旋转限制;.flip() 和 .flop() 相同;.modulate({ brightness, saturation }) 也相同。输出格式方法同样齐全,包括 .webp({ quality })、.jpeg({ quality })、.png()、.avif() 和 .heic()。
第 4 步——替换终结方法。 Sharp 的 .toBuffer() 对应 Bun.Image 的 .toBuffer(),但后者返回 Uint8Array 而不是 Buffer;如果下游代码会检查 Buffer 类型,这一点很重要。Sharp 的 .toFile(path) 则改为 .write(path)。两者的惰性管线约定完全一致:只有 await 终结方法后,处理才会真正执行。
下面是某个路由处理函数的真实 diff:
// before
import sharp from "sharp";
export async function processCover(input: Uint8Array) {
const buf = await sharp(input)
.resize(1200, 630, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
return buf;
}
// after
export async function processCover(input: Uint8Array) {
const buf = await Bun.image(input)
.resize(1200, 630, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
return buf;
}常见场景的全部改动就是这些:删除一行 import,替换一次构造函数调用。
迁移完成后,bun pm ls | grep sharp 不再返回任何内容。CI Dockerfile 也删掉了 RUN apk add --no-cache vips-dev。最终镜像缩小约 80MB,package.json 则少了一个依赖和一条 peer-dep 警告。
目前仍无法直接对应的 3 个能力
在开始删除 Sharp 之前,必须先正视下面这些差异。如果你的管线不只是单纯地缩放和编码,其中至少有一项很可能会成为问题。
问题 1——ICC 色彩配置文件透传。 Sharp 的 .withMetadata({ icc: "p3" }) 能在编码后保留输入图像的色彩配置文件。截至 1.3.14,Bun.Image 会移除 ICC。对“输入 sRGB、输出 sRGB”的流程而言——绝大多数 Web 图像任务都属于这一类——肉眼看不出差异。但在摄影管线中,如果用户上传广色域 Display-P3 图像并要求保留配置,Sharp 依然胜出。必须使用 Bun.Image 时,可以用 exifr 读取 ICC 数据块,编码完成后再手动附加回去。这个方案并不优雅。
问题 2——动图 WebP 与 GIF 帧。 Bun.Image 只解码动图输入的第一帧,其余帧会被丢弃。Sharp 的 { animated: true } 与逐帧访问目前没有对应能力。只要业务涉及精灵图处理、动图缩略图生成或任何逐帧操作,这就是无法绕过的限制,应当在这些代码路径上保留 Sharp。
问题 3——.tile() 金字塔。 Sharp 继承了 libvips 的 deepzoom / IIIF 切片生成能力。如果你在运行 Leaflet 风格的图像服务器、地图切片管线或博物馆级缩放界面,这并非可选功能。Bun.Image 没有切片原语,而且短期内大概率也不会提供;libvips 积累了几十年的工程成果,Bun 团队势必先覆盖更常见的需求。
还有一个较小但值得提醒的问题: Sharp 的 .rotate(45) 可通过双线性插值实现任意角度旋转。Bun.Image 的 .rotate() 只接受 90、180 和 270。对 99% 的封面图与商品缩略图处理来说,这无关紧要;但如果需要倾斜校正或视觉倾斜效果,它就是阻断项。
现在,只要负载会触及上述任一情况,我都会采用双栈模式:常规路径使用 Bun.Image,只有问题场景才在 worker 线程中加载固定版本的 Sharp:
async function process(input: Uint8Array, meta: ImageMeta) {
if (meta.hasICC || meta.isAnimated || meta.needsTile) {
const sharp = (await import("sharp")).default;
return sharp(input)
.resize(1200, 630, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
}
return Bun.image(input)
.resize(1200, 630, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
}动态 import 可以让那些永远不会进入慢路径的部署目标彻底不打包 Sharp。
CI 安装与冷启动实测数据
我最看重的是安装耗时的变化,因为 CI 分钟数会不断累积。
在我的 Ubuntu x86_64 CI runner 上,固定使用 Sharp 时,bun install 热安装耗时 4.8s;从 package.json 移除 Sharp 后,只需 1.4s。节省的时间来自跳过 Sharp 预编译二进制文件下载,以及可选 libvips 系统依赖的探测。
冷安装(没有 ~/.bun/install/cache,也没有 node_modules)则从 18.2s 降到 7.1s。通常,从一棵包含 200 个包的依赖树中移除一个原生插件,不会带来如此明显的变化;这次降幅格外大,是因为 Sharp 的 postinstall 原本就是整棵依赖树中最慢的单一步骤。
Bun 1.3.14 还为隔离 linker 带来了全局存储,发布说明称整个项目的“热安装提速 7 倍”。叠加移除 Sharp 的收益后,管理后台仓库完整的 bun install 热安装周期从 6.4s 降至 1.1s。在本地开发中,这种变化能被直接感知:bun add some-package 再也不够你去冲杯咖啡。
移除 Sharp 的预编译二进制文件以及预编译回退路径所需的 Alpine vips-dev 包后,Docker 镜像缩小了约 80MB。CI 的层缓存命中率也有所提高,因为安装步骤下方的层能在更多依赖变化中保持稳定;Sharp 的预编译二进制下载曾是更容易触发缓存失效的因素之一。
我一直倾向于保持开发工具链精简;出于同样的考虑,我曾在 Claude Code 2.1.141 hooks 上线当天就将它们投入使用。对独立开发者而言,正是这些微小的工具链收益不断叠加,才形成了竞争力。一次 bun install 快 5 秒听起来不算多,但把它乘以每周 80 次提交,意义就完全不同。
我最担心的指标其实是单图延迟。以一项有代表性的缩放任务为例:把 1024×1024 PNG 转成质量 82 的 512×512 WebP。在本地 M2 上,Sharp 0.34.2 与 Bun.Image 的结果相差不超过 8%。对 Web 负载而言,这点差距无论偏向哪一方都不重要。
Bun.Image 只有 18 个月历史,却能在原始缩放性能上与积累了 20 年的 libvips 竞争,架构上的原因在于 i16 定点 SIMD 缩放内核,以及 JPEG 解码时用 IDCT 缩放到满足需求的最小尺寸。它不会先把 4000×4000 JPEG 完整解码成位图再缩放,而是直接按目标分辨率解码。Sharp 通过 libjpeg-turbo 使用了相同技巧,因此两套管线的结果处于同一水平。
内存表现则是 Bun.Image 明显领先的地方:零拷贝借用 ArrayBuffer,使其在批量处理 50+ 张图像时的峰值 RSS 低于 Sharp。如果一个 worker 调用会处理整组图集,这项优势很重要;如果每次请求只处理一张图,则几乎感受不到。
哪些场景 Sharp 仍然占优,什么时候该迁移
遇到以下任一情况,请继续使用 Sharp:
- 需要逐帧访问动图 WebP。
- 摄影工作流需要保留广色域图像的 ICC 色彩配置文件。
- 图像服务器或地图切片业务依赖
.tile()深度缩放金字塔。 - 需要带插值的任意角度旋转。
- 部署目标只支持 Node,例如 Vercel Node functions、AWS Lambda 的 Node runtime、Cloudflare Workers(目前尚无 Bun),因而无法选择 Bun。
同时满足以下条件,则适合迁移到 Bun.Image:
- 至少有一层已经运行在 Bun runtime 上。
- 图像任务主要是以 JPEG、PNG、WebP、AVIF 或 HEIC 完成“解码、缩放、重新编码”。
- CI 已经受够 Sharp 预编译二进制文件的重建,或者使用 Alpine 容器并撞上了 libvips 系统依赖这堵墙。
截至 2026 年 5 月,客观来看,Bun.Image 已覆盖“Sharp 常见场景的 95%”,安装体积更小,也完全省去了原生插件的配套工作。剩余 5% 正是 Sharp 凭借 libvips 深厚积累继续发挥价值的部分,因此应当预留一段双栈期,不要第一天就从所有位置删掉 Sharp。先迁移落在 Bun.Image 优势区间内的路由,其他路由继续交给 Sharp;等 Bun 1.4.x 很可能补上任意角度旋转和动图帧后,再重新评估。
后续升级重点关注三项能力:任意角度旋转、逐帧访问动图 WebP,以及 ICC 透传。以 Rust 重写后的工程推进速度来看,它们最有可能成为下一批上线的功能。订阅 Bun changelog,并在每个 minor 版本发布后重新审视双栈边界。
我会直接合入生产的 Bun Sharp 迁移提交
下面是迁移完成后的一条生产路由,其中包含错误处理和 engines 版本约束:
// package.json
// "engines": { "bun": ">=1.3.14" }
import { Hono } from "hono";
const app = new Hono();
app.post("/api/uploads", async (c) => {
const form = await c.req.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return c.json({ error: "no file" }, 400);
}
const input = new Uint8Array(await file.arrayBuffer());
try {
const webp = await Bun.image(input)
.resize(1200, 630, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
const jpeg = await Bun.image(input)
.resize(1200, 630, { fit: "cover" })
.jpeg({ quality: 84 })
.toBuffer();
await Bun.s3().write(`covers/${crypto.randomUUID()}.webp`, webp);
await Bun.s3().write(`covers/${crypto.randomUUID()}.jpg`, jpeg);
return c.json({ ok: true });
} catch (err) {
return c.json({ error: String(err) }, 500);
}
});
export default app;有三件事值得显式固定下来。
package.json 中的 "engines": { "bun": ">=1.3.14" } 不是可有可无。Bun.Image 从 1.3.14 才开始提供;旧版本会在运行时报出 Bun.image is not a function。最好让它在安装时暴露为错误,而不是上线后返回 500。
bun-types 包(用于取代 @types/bun)从 1.3.14 开始提供 Bun.Image 类型定义。tsc --noEmit 无需 @ts-expect-error 垫片即可通过。如果编辑器仍然在 Bun.image 下方标红,说明固定的 bun-types 版本过低。
回滚方案是:先让 sharp 在 optionalDependencies 中保留一个发布周期,同时以前文问题章节中的动态 import 双栈作为回退路径。生产指标连续一周正常后,再从 optionalDependencies 中移除 sharp 并删除回退分支。不要在同一次提交中完成这两件事;如果格外谨慎,也不要安排在同一周。
判断一项 Bun 功能能否用于生产,不能只看 changelog 怎么说,而要看你是否愿意亲手把它写进自己的提交。这个版本,我会发布。
Bun.Image 能在 Bun 以外的环境(Node.js)运行吗?
不能。Bun.Image 是运行时内置能力,不是 npm 包。如果需要同时兼容 Node 和 Bun 的 Sharp 替代方案,可以考虑 bun-image-turbo 等第三方包,或者继续使用 Sharp。
Bun.Image API 真能直接替换 Sharp,还是只借鉴了它?
它的链式结构和方法名是有意与 Sharp 兼容的:构造函数 → .resize / .rotate / .flip / .modulate → .webp / .jpeg / .png / .avif 终结方法。多数调用位置只需替换 import 那一行。四项差异分别是任意角度旋转、动图帧、ICC 透传和 .tile()。
Bun.Image 底层使用了什么?
JPEG 解码与编码使用 libjpeg-turbo,PNG 使用 spng,WebP 和 AVIF 使用 libwebp,几何运算则使用 Bun 自有的 SIMD 内核(i16 定点缩放)。它们全都编译进 Bun 二进制文件,无需原生插件,也没有重建步骤。
Bun.Image 与 Sharp 的缩放性能相比如何?
在常见的 JPEG/PNG 缩放与重新编码任务中,两者在本地硬件上的性能差距不超过约 8%。面对超大图像的流式处理和动图负载时,Sharp 的 libvips 仍然更快。Bun.Image 更明显的优势在于安装时间和内存,而非单次缩放的原始 CPU 性能。
现在应该迁移吗?
如果项目运行在 Bun 上,图像管线是在 JPEG/PNG/WebP/AVIF 之间进行解码、缩放和重新编码,可以迁移。如果依赖动图 WebP 帧、ICC 色彩配置文件保留或 .tile() 深度缩放,则应在这些路径继续使用 Sharp,并维持双栈。
2026年9月5日







