从云相册的缩略图说起:Bun.Image 让我告别 sharp

25年初我写过一个云相册 app,里面有一个非常常见的场景:相册页。

用户打开一个相册,里面动辄几十上百张照片。如果我老老实实地把原图丢给前端去加载,会发生什么?带宽炸了,首屏废了,用户划两下就关掉了。这种场景下,缩略图不是优化项,是必选项

当时我用的是 sharp。功能没得说,是 Node.js 生态里事实上的标准。但中间踩了一些跨平台和预编译二进制相关的坑------具体是哪一次部署、什么环境,我已经记不清了,只记得当时被折腾得很难崩。后来回想,问题其实从来不是 sharp 本身,而是"图像处理"这件事,被绑死在了 native 模块的分发问题上:你要装 libvips,要匹配 glibc,要照顾 M1、Alpine、Lambda......每多一个目标环境,就多一份心智负担。

然后 Bun 进了我的工具箱

25年九月,Anthropic 收购了 Bun ,当时我也去围观了一下,感觉 Bun 的开箱即用特性很吸引我。所以后来,我开始把 Bun 当成日常写小工具的脚手架。

最典型的例子是博客封面压缩。我现在写文章的封面基本都让 AI 生成,问题是 AI 出图动辄三五兆,直接塞进文章里就是带宽刺客。所以我写了一个小 CLI,单文件,bun run 直接跑,做的事很简单:读图、缩到合理尺寸、转 WebP、写出去。

Bun 让这种"写个工具"的事情重新变得轻:单文件即可,TypeScript 直接跑,启动快,依赖少。但唯一的"重"还是图像处理本身------我仍然要装 sharp(虽然安装不麻烦,但是想起之前的坑总让人感到别扭)。

Bun v1.3.14

Bun v1.3.14 的发布说明里有一行字让我愣了一下:

Bun now ships a built-in image processing API ... designed as a drop-in alternative to sharp for common server-side image operations.

runtime 自己把图像处理收编了,零 native 模块。

一行话能力概览

  • 格式:JPEG / PNG / WebP / GIF / BMP 全平台支持(静态链接,输出一致);HEIC / AVIF / TIFF 在 macOS 走 ImageIO + vImage,Windows 走 WIC
  • 输入 :路径字符串、ArrayBuffer / TypedArray(零拷贝)、Blob / BunFile / S3Filedata: URL
  • 变换.resize() / .rotate() / .flip() / .flop() / .modulate(),链式调用
  • 编码.jpeg() / .png() / .webp() / .heic() / .avif(),每种格式带自己的质量参数
  • 终结.bytes() / .buffer() / .blob() / .toBase64() / .dataurl() / .metadata() / .write(dest),外加一个有意思的 .placeholder()
  • Body 集成Bun.Image 实例可以直接作为 Response 的 body,Content-Type 自动带上

用我自己的两个场景重写

场景一:云相册缩略图。 上传接口里直接出缩略图,一行表达完:

ts 复制代码
// 上传一张图,返回 200px 宽的 WebP 缩略图
return new Response(new Bun.Image(upload).resize(200).webp({ quality: 80 }))

昨年我在 sharp 里要写流处理、要管 buffer、要设 header,现在这些事 runtime 全替我办了。

场景二:博客封面压缩 CLI。 我那个 CLI 的核心逻辑大概会变成这样:

ts 复制代码
await Bun.file('cover.png')
  .image()
  .resize(1600, null, { fit: 'inside', withoutEnlargement: true })
  .webp({ quality: 82 })
  .write('cover.webp')

这意味着------CLI 的依赖里,可以把 sharp 整行删掉了。

一个让我多看了两眼的 API:.placeholder()

ts 复制代码
const placeholder = await Bun.file('hero.jpg').image().placeholder()
// thumbhash 的 data URL,可以直接拿去做 blur-up 占位

返回的是 thumbhash 编码的 data URL。过去要做这个效果,得引一个 blurhash/thumbhash 库,自己在客户端解码。现在 runtime 一行给你。

性能

Bun 官方拿 sharp 0.34.5 做了对比,在 linux/x64 上跑了 50 次迭代。具体数字我不复述,建议感兴趣的同学看原文,或者在自己的目标环境上跑一遍。

它给出的实现关键词倒是值得记一下:i16 定点 SIMD 的 resize 内核JPEG IDCT 直接缩放到刚好够用的尺寸ArrayBuffer 零拷贝借用预分配 arena 给 resize 的临时内存 。除了 metadata(),所有处理都在主线程之外跑。这套组合拳很 Bun。

我的视角

抛开"快不快",Bun.Image 让我感兴趣的其实是另外几个东西:

第一,native 模块的分发问题被消化进了 runtime。 这和 Bun.sqlBun.S3FileBun.serve 是同一种思路------常用的能力,runtime 自己扛。这意味着我那个图片压缩 CLI 现在可以做到真正意义上的单文件、零依赖、跨平台。对独立开发者,这是实打实的红利。

第二,drop-in sharp,但不是替代 sharp。 迁移成本几乎为零,API 形态几乎一致。但 Bun.Image 现在覆盖的是"常见服务端图像操作"------composite、SVG 渲染、ICC profile、复杂的 pipeline 组合,这些 sharp 仍然更全。它解决的是 80% 场景的依赖问题,不是要把剩下的 20% 也吃掉。

第三,HEIC/AVIF 走 OS 后端是个有趣的取舍。 能力换体积:在 macOS 和 Windows 上你白拿了苹果和微软已经做好的 codec;但反过来,Linux 上目前没有这两种格式。如果你的服务端跑在 Linux 容器里,HEIC/AVIF 这块现在还得另想办法。这点值得提前知道,免得部署时才发现。

回到云相册

如果今天让我重做那个云相册的缩略图,我大概率会写:

ts 复制代码
Bun.serve({
  routes: {
    '/thumb/:id': async (req) => {
      const file = await getOriginal(req.params.id)
      return new Response(new Bun.Image(file).resize(400).webp({ quality: 80 }))
    },
  },
})

然后就没有然后了。

当 runtime 把基础能力收编,工具的复杂度就回归到了业务本身。这种"由下而上挪走的复杂度",可能才是 Bun 最值得期待的事情。

相关推荐
ljt27249606612 小时前
Vue笔记(三)--用户交互
javascript·vue.js·笔记
Martin -Tang2 小时前
uniapp 实现录音操作,长按录音,放开取消
前端·javascript·vue.js·uni-app·css3·录音
ZC跨境爬虫3 小时前
跟着 MDN 学 HTML day_58:(构建行星数据表——HTML表格高级实战指南)
前端·javascript·ui·html·音视频
kyriewen3 小时前
用户打开飞行模式都能打开你的网站?Service Worker 做离线缓存,PWA 实战
前端·javascript·面试
栉甜3 小时前
APIs学习
前端·javascript·css·学习·html
zithern_juejin4 小时前
ES6——Symbol
javascript
代码煮茶4 小时前
Vue3 组件库二次封装实战 | 基于 Element Plus 封装企业级 UI 组件库
前端·javascript·vue.js
shen_4 小时前
JS语法:生成器和可迭代对象
javascript
之歆4 小时前
DAY_11JavaScript BOM与DOM深度解析:底层原理与工程实践(上)
开发语言·前端·javascript·ecmascript