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 Image 实战:4 步从 Sharp 迁移到 Bun 1.3.14

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:

Bash
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:

TypeScript
// 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:

TypeScript
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 版本约束:

TypeScript
// 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,并维持双栈。

发布日期
分类
Build
ACP 协议详解:换编程智能体,不必换编辑器

ACP 协议详解:换编程智能体,不必换编辑器

ACP 协议如何让编程智能体接入编辑器?本文梳理 Zed、JetBrains 和 Android Studio 的支持方式,解释 BYOA、会话流程、适配器与原生支持的差异,并对比 ACP 和 MCP。了解 Claude Agent 接入步骤,以及团队试点前必须核实的身份验证、配置、会话恢复和权限边界。2026年11月3日Build
2026 年 Vercel 替代方案:Next.js 与 AI 应用该迁到哪里?

2026 年 Vercel 替代方案:Next.js 与 AI 应用该迁到哪里?

正在寻找 Vercel 替代方案?本文对比 Cloudflare、Render、Railway、Netlify、Fly.io 与 AWS Amplify,梳理 Next.js 和 AI 应用的托管费用、免费额度、流式响应支持、后台任务及迁移限制,帮助你按实际负载选平台,并判断何时继续使用 Vercel 更合适。2026年11月3日Build
Vapi 替代方案 2026:Retell AI、Bland AI 等平台怎么选

Vapi 替代方案 2026:Retell AI、Bland AI 等平台怎么选

寻找 Vapi 替代方案?对比 Retell AI、Bland AI、ElevenLabs Agents、Synthflow、Vida Global 与 CloudTalk,厘清 AI 计费、运营商费用、并发限制和集成要求。先验证预约等业务操作与转人工,再决定是否迁移主号码,避免只看每分钟标价。2026年11月3日Build
浏览器自动化入门:用 Browser Use 搭建 AI 智能体

浏览器自动化入门:用 Browser Use 搭建 AI 智能体

想用 AI 完成网页任务,又把自动化留在自己的应用中?本文介绍 Browser Use 的工作原理、模型支持与 Cloud 部署选择,带你从只读任务开始,搭建浏览器智能体。文中还梳理登录状态保护、提示词注入、限流与重试的边界,说明如何核验实际结果,以及何时应保留 API、固定脚本或人工提交。2026年11月2日Build
Zernio评测:社交媒体 API 值不值得接入?

Zernio评测:社交媒体 API 值不值得接入?

Zernio评测聚焦开发者真正需要的社交媒体API能力:支持哪些平台、账号要满足什么条件、如何按连接数和实际用量计费,以及发布、收件箱、广告和MCP智能体的边界。结合公开文档与工作流,对比Ayrshare、Postiz和原生API,帮助独立开发者、SaaS团队及代理机构判断是否适合接入自己的产品。2026年11月1日Build
Airtable vs Notion:数据库、知识库与项目管理,团队怎么选?

Airtable vs Notion:数据库、知识库与项目管理,团队怎么选?

Airtable vs Notion 怎么选?本文按轻量 CRM、内容日历、项目管理、团队知识库、库存与客户门户逐项比较,梳理价格、数据库容量、自动化、API 限制与 AI 积分规则,说明客户权限、表单更新和迁移时容易忽略的边界,并给出搭配使用的分工思路,帮助团队根据实际工作决定选哪款。2026年11月1日Build
Google Jules 停服指南:导出工作记录,迁移后台任务

Google Jules 停服指南:导出工作记录,迁移后台任务

Google Jules 即将停服,如何保住代码仓库之外的任务记录?本文梳理只读与关停时间表、官方导出流程、历史记录和 API 的未决问题,并说明如何迁移到 Antigravity、Codex、Claude Code 或 GitHub Copilot,验证环境配置、测试和代码审查流程,再切换定期任务。2026年11月1日Build
smolagents 教程:Python 代码智能体原理、沙箱与框架选型

smolagents 教程:Python 代码智能体原理、沙箱与框架选型

smolagents 如何用 Python 驱动 AI 智能体?了解两种智能体的区别、工具与模型的搭配,以及 Docker 示例的执行流程。本文梳理沙箱边界、权限与资源限制,并对比 LangGraph 和 OpenAI Agents SDK,帮助开发者判断何时适合动态组合代码,何时应采用持久化工作流或专职智能体协作。2026年10月31日Build
订阅通讯

每周日,一封信。写运转中的系统,不写热评。

每周一期。无垃圾邮件。随时退订。