前端和 Node.js 社区对图像处理这件事一直有个心结:Sharp。

Sharp 很快,基于 libvips,处理几十兆的大图也不带喘气的。但它有个老毛病,部署麻烦。它依赖原生 C++ 扩展,换一台服务器、换一个操作系统、换一个 Docker 基础镜像,都可能要重新编译。CI/CD 环境里踩坑的人不计其数,Serverless 平台上更是重灾区。

Bun 团队盯上了这个痛点。

Bun.Image 来了

5 月 2 日,Bun 官方在 X(原 Twitter)上宣布:**下一个版本将内置 Bun.Image**,一个快速的多格式图像处理库,零依赖,开箱即用。

这条推文 161.5 万次浏览,195 条评论,5249 个点赞,算是把整个 JS 社区都惊动了。

先看最直观的代码:

await Bun.file("photo.jpg")
  .image()
  .resize(1024, 1024, { fit: "inside" })
  .rotate(90)
  .webp({ quality: 85 })
  .write("thumb.webp");

链式调用,读文件、转图像对象、缩放、旋转、编码为 WebP、写入磁盘,一气呵成。熟悉 Bun 文件 API 的开发者会感觉很自然,因为入口就是 Bun.file(),跟读写普通文件一样。

核心功能拆解

格式支持

JPEG、PNG、WebP 在所有平台通过 libjpeg-turbo、libspng 和 libwebp 静态链接支持,不需要系统预装任何东西。

HEIC 和 AVIF 稍有不同,它们依赖操作系统内置的编解码能力:

  • macOS:通过 ImageIO/CG 支持,HEIC 要求 10.13+,AVIF 要求 13+

  • Windows:通过 WIC 支持,HEIC 需要 Win10 1809+ 并安装 HEIF Image Extensions,AVIF 需要 Win10 并安装 AV1 Video Extension

  • Linux:不支持 HEIC 和 AVIF,调用会抛出 UnsupportedOnPlatform

这个设计有取舍。Linux 上的 HEIC/AVIF 支持缺失,对于跑在 Linux 服务器上的生产环境是个局限,但 Bun 团队选择了用系统 API 避免捆绑更多二进制,也算务实。

Resize 与 filter

缩放 API 提供了丰富的控制选项:

img.resize(800);                              // 宽 800,等比缩放
img.resize(800, 600);                         // 强制 800×600(拉伸)
img.resize(800, 600, { fit: "inside" });      // 保持比例,放入 800×600 框内
img.resize(800, 600, { withoutEnlargement: true }); // 永不放大
img.resize(800, 600, { filter: "mitchell" }); // 指定重采样核

fit 参数有两个选项:

  • "fill"(默认):拉伸到指定宽高

  • "inside":保持宽高比,结果图像完整放入目标框

filter 参数控制缩放算法,选项很全:

filter

适用场景

"lanczos3"

(默认)

通用,照片最清晰

"lanczos2"

稍柔和,振铃伪影更少

"mitchell"

平滑渐变,经典双三次折中

"cubic"

Catmull-Rom,比 mitchell 锐,有振铃风险

"mks2013"

 / "mks2021"

"Magic Kernel Sharp",Facebook/Instagram 用

"bilinear"

 / "linear"

快速,柔和

"box"

面积平均,适合整数倍缩小

"nearest"

像素画 / 硬边缘

缩放在 ARM64 和 x64 上均做了 SIMD 加速,性能不会太差。

还有一个细节值得注意:当源文件是 JPEG,且目标尺寸不超过源尺寸一半时,解码会直接跳到最近的 M/8 IDCT 缩放级别,不会先解码出完整的全分辨率图像再缩小。对于处理 2400 万像素的手机照片来说,这意味着内存峰值会大幅降低。

与 Bun.s3 协作

Bun.Image 和 Bun.s3 是天然搭档:

// S3 in, S3 out
await Bun.s3("uploads/raw.heic")
  .image()
  .resize(800)
  .jpeg({ quality: 85 })
  .write(Bun.s3("cdn/out.jpg"));

从 S3 读取原始图像,缩放后编码为 JPEG,直接写回 S3,中间不需要落本地磁盘,也不需要额外安装任何 SDK 或图像库。对于做图像 CDN 或用户上传处理的场景,代码量能少一半以上。

剪贴板读取

macOS 和 Windows 上还支持从剪贴板读取图像:

// ⌘↑4 → 优化后的 PNG,就这一行
await Bun.Image.fromClipboard()
  ?.resize(800, 800, { fit: "inside" })
  .png({ palette: true, colors: 64 })
  .write("shot.png");

Linux、FreeBSD 和 Android 上此方法返回 null。对于写截图工具或本地工具脚本的开发者,这个 API 还挺有用。

Placeholder 低质量占位图

const thumbnail = await file(process.argv.at(-1)).image().placeholder();
console.log(`<img src=${thumbnail} />`);

.placeholder() 生成 LQIP(低质量图像占位图),输出 base64 编码的图像数据 URL,可以直接内联到 HTML 里。页面加载时先展示模糊缩略图,等原图加载完再替换,这是现代 Web 图像懒加载的标准做法,Bun 帮你一行完成。

社区吵什么

这个功能引发了 JS 社区的老生常谈争论:运行时应该内置多少东西?

反对声音的逻辑很一致:"这是臃肿(bloat),直接用 Sharp 就行。"

站出来力挺的是 dax(@thdxr,OpenCode 创始人),他的观点很直接:

像往常一样,一群人说「这太臃肿了,直接用库就行了」。图像缩放尤其受益于原生代码,而像 Sharp 这样的库部署到所有环境总是很麻烦,因为你需要构建原生部分。

这不是在说 Sharp 不好,Sharp 本身质量没问题。问题出在部署链路上。Sharp 的原生扩展需要在目标环境编译,或者预编译的二进制文件要匹配 glibc 版本、CPU 架构、Node.js 版本。Serverless 函数冷启动、Docker 多架构镜像、跨系统 CI,都是它容易出问题的地方。踩过坑的人都知道那种感受。

Bun 选择内置,等于把"部署能跑起来"这件事的责任揽过来了。用户不用关心底层库,Bun 版本升级时也统一维护。

这跟当年 Node.js 和 Deno 的路线分歧是同一个问题的延伸。Deno 选择内置更多工具(格式化、测试、类型检查、权限沙箱),Node.js 保持精简靠生态补全。Bun 明显偏向 Deno 的思路,但在性能上下的功夫更多。

能不能说哪条路绝对正确?不能。但对于想快速上手、不想踩原生依赖坑的开发者,Bun.Image 是实实在在的减负。

现阶段的局限

有几点需要说清楚:

Linux 上 HEIC/AVIF 不支持,如果你的服务器跑 Linux,这两种格式用不了。现代手机拍的照片很多是 HEIC,这是个实际限制。

功能目前只在下一个版本,尚未正式发布,API 有可能在正式版前做调整。

Sharp 的功能更全,锐化、模糊、颜色空间操作、合成等高级功能,Bun.Image 当前公布的 API 里还看不到。重度图像处理场景,Sharp 仍然是更成熟的选择。

值得关注的趋势

Bun 这两年一直在往运行时里塞东西,内置 SQLite、内置 S3 客户端、内置测试框架、内置打包器,现在加上图像处理。它的策略很明确:尽量让开发者在 Bun 运行时本身就能搞定常见需求,减少对第三方包的依赖。

对 Node.js 生态来说,这是一种压力,也是一种参照。Node.js 核心不会很快跟进内置图像处理,但 Bun 做给大家看了:运行时可以处理这类事情,而且能处理得不错。

更多推荐