Bun 画像処理をSharpから移行:Bun.Image 1.3.14実践ガイド
Bun 1.3.14のBun.Imageで、Sharp依存のBun 画像処理を4ステップで移行する実践ガイド。実際のコード差分、CI時間とDocker容量の改善値、ICC・アニメーションWebP・tile()など未対応機能、二重構成による段階的なロールバック方法まで詳しく解説します。

Bunは2026年5月13日、Bun.Imageを搭載したv1.3.14を公開しました。libjpeg-turbo、spng、libwebpを組み合わせたパイプラインで、Sharp APIに近い操作性を備えながら、ネイティブアドオンを一度もビルドせずにBun 画像処理を実行できます。Nodeを更新するたびにlovell/sharpのlibvipsバイナリが原因でCIが失敗する状況を3年間経験し、私はこのバージョンを機にSharpを外しました。
Bun 画像処理へ移行し、1.3.14公開日にSharpを外した理由
Bun 1.3.14のリリースノートが公開されたのは5月13日です。いつもの機能一覧で「HTTP/3 client」や「7x faster warm installs」の陰に隠れていたのが、私に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のレイヤーキャッシュが無効になり、pushのたびにnpm rebuild sharpがゼロから走る。Vercelのビルドはビルド済みバイナリのCDNへ接続したままタイムアウトする。3つの別々のプロジェクトでこれを3年間繰り返し、いつしかapk add --no-cache vips-devを手癖で書くようになっていました。
Bun.Imageは、この問題に対するランタイムネイティブな答えです。画像処理のためのnpm installは不要で、コーデックはBunバイナリに同梱されています。Nodeもアドオンもないため、Node ABIが変わるたびにネイティブアドオンを再ビルドする必要もありません。ジオメトリカーネルにはi16固定小数点SIMDを採用し、JPEGのデコード時には必要十分な最小サイズへ自動的に縮小します。構造でいえば、SharpがNodeアドオンである必要がなかった場合に実現していたであろう姿です。
1.3.14を重視した理由はもう1つあります。Anthropicが資金提供するRustへの書き換えが入る前、これが最後のZigリリースだからです。The Registerは5月14日にマージの進行ペースを報じており、今後の開発速度はかなり上がるはずです。ランタイムの書き換えをまたいでネイティブアドオン依存を抱え続けるより、今のうちにBunのプリミティブへ移行する方を選びました。
これはSharpへの弔辞ではありません。libvipsを使うSharpは、アニメーションWebP、カラープロファイルが重要な写真処理、tile()によるDeep Zoomピラミッドでは、今なお速度面で最強です。ここから紹介するのは、その3つに当てはまらない画像処理、つまり本番アプリの大半を占めるデコード、リサイズ、エンコードという95%の用途に向けた移行手順です。
カバー画像パイプラインをSharpから移行する4ステップ
今回置き換えたのは、gpt-image-2が生成したカバー画像をR2へ送る前に後処理するワーカーomidsaffari-adminです。PublishWorkflowのcoverステップ後に通る1ファイル内に、Sharpの呼び出し箇所が8つありました。WebPとJPEGフォールバックの両方をエンコードする経路を含め、移行に要した時間は合計42分です。
ステップ1 — Sharpの利用範囲を洗い出します。 変更を始める前に、すべてのimportを探します。
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/先に把握すべきなのは、置き換える場所が4か所なのか40か所なのかです。40か所なら、一度にすべてを変えず、ルート単位で進めます。
ステップ2 — importをBun.file().image()に置き換えます。 Sharpのコンストラクタにはパス、Buffer、Streamを渡せます。Bun.Imageのコンストラクタは、Bun.file()経由のパス、Uint8Array、Blob、またはBunのファイルプリミティブが返す任意の値を受け取ります。Bun.s3()の参照もそのまま扱えるため、私のコード構造も変わりました。
ステップ3 — メソッドチェーンを対応付けます。 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()です。ただし、返り値はBufferではなくUint8Arrayなので、受け側がBufferを型チェックしている場合は注意が必要です。Sharpの.toFile(path)は.write(path)に変わります。終端メソッドをawaitするまで何も実行されない遅延パイプラインの契約は同じです。
実際のルートハンドラーで行った差し替えは次のとおりです。
// 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が1行、コンストラクタの呼び出しが1か所です。
移行後は、bun pm ls | grep sharpを実行しても何も返りません。CI用DockerfileからはRUN apk add --no-cache vips-devの行を削除できました。生成されるイメージは約80MB小さくなり、package.jsonからは依存関係が1つ、peer dependencyの警告が1つ減りました。
Bun.Imageへそのまま移せない3つの機能
画像処理がリサイズとエンコードだけで完結していないなら、Sharpを外し始める前に次の点を率直に確認してください。少なくとも1つは問題になる可能性があります。
注意点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のDeep Zoom / IIIFタイル生成を継承しています。Leaflet型の画像サーバー、地図タイルのパイプライン、美術館水準のズームUIでは欠かせません。Bun.Imageにタイル用プリミティブはなく、当面追加されない可能性が高いでしょう。libvipsには数十年分の蓄積があり、Bunチームはまず一般的な用途を優先するはずです。
もう1つ、小さいながら触れておくべき注意点があります。 Sharpの.rotate(45)は、バイリニア補間を使った任意角度の回転に対応します。Bun.Imageの.rotate()が受け付けるのは90、180、270だけです。カバー画像や商品サムネイルの処理の99%では問題になりませんが、傾き補正や見た目の演出として画像を傾ける場合は移行を妨げます。
現在、上のいずれかに該当するワークロードでは、通常経路をBun.Imageで処理し、問題のあるケースだけワーカースレッドに固定した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ランナーでは、Sharpを固定した状態のbun installはウォーム時に4.8sかかりました。Sharpをpackage.jsonから外すと1.4sです。短縮できたのは、Sharpのビルド済みバイナリのダウンロードと、任意のlibvipsシステム依存を調べる処理がなくなったためです。
コールドインストール(~/.bun/install/cacheもnode_modulesもない状態)は18.2sから7.1sになりました。200パッケージある依存ツリーからネイティブアドオンを1つ外しただけで、通常はここまでインストール時間は変わりません。大幅に短縮したのは、Sharpのpostinstallがツリー内で最も遅い単一処理だったためです。
Bun 1.3.14には分離リンカーのグローバルストアも含まれ、リリースノートではプロジェクト全体の「7x faster warm installs」と説明されています。Sharpの削除と組み合わせると、管理リポジトリ全体のbun installウォームサイクルは6.4sから1.1sへ短縮しました。ローカル開発中にも体感できる変化で、bun add some-packageのたびにコーヒー休憩を取る必要がなくなります。
Sharpのビルド済みバイナリと、ビルド済み版のフォールバックが要求するAlpineのvips-devパッケージを削除すると、Dockerイメージは約80MB小さくなりました。インストールステップより下のレイヤーが依存関係の変更後も安定しやすくなるため、CIのレイヤーキャッシュヒット率も上がります。Sharpのビルド済みバイナリのダウンロードは、キャッシュを無効にしやすい要因の1つでした。
普段から開発ツールチェーンは絞り込んでいます。同じ考えから、Claude Code 2.1.141のhooksも公開日に導入しました。こうした小さな改善の累積こそ、1人で運営する開発体制の競争力になります。bun installが5秒速くなっても、大した差には聞こえないかもしれません。週80回のコミットを掛け合わせてみてください。
画像1枚あたりのレイテンシーは、最も不安だった指標です。代表的な処理として、1024×1024のPNGを品質82の512×512 WebPへリサイズしたところ、ローカルのM2ではSharp 0.34.2とBun.Imageの差が8%以内に収まりました。Webワークロードでは、どちらの差も問題になりません。
登場から18か月のBun.Imageが、20年の歴史を持つlibvipsに対して生のリサイズ性能で競えるのは、アーキテクチャに理由があります。i16固定小数点SIMDのリサイズカーネルに加え、JPEGのデコード時にはIDCTスケーリングによって必要十分な最小サイズへ縮小します。4000×4000のJPEGをフルサイズのビットマップへ展開してから縮小するのではなく、Bun.Imageは対象の解像度で直接デコードします。Sharpもlibjpeg-turbo経由で同じ工夫を使うため、両パイプラインは近い性能に落ち着きます。
メモリでは、Bun.Imageの優位がはっきりします。ArrayBufferをゼロコピーで借用するため、50枚超の画像をまとめて処理したときのピークRSSがSharpより低くなります。1回のワーカー呼び出しでギャラリーを処理するなら重要です。リクエストごとに画像を1枚だけ処理するなら、利点は見えません。
Sharpを残すケース、Bun.Imageへ移行するケース
次のいずれかに当てはまるなら、Sharpを残します。
- アニメーションWebPのフレームへ個別にアクセスする必要がある。
- 広色域の写真処理でICCカラープロファイルを保持する。
- 画像サーバーや地図タイルの処理で
.tile()のDeep Zoomピラミッドに依存している。 - 補間を使った任意角度の回転が必要である。
- Node専用のデプロイ先で動かしている。たとえばVercel Node functions、AWS LambdaのNode runtime、Cloudflare Workers(まだBunはありません)では、Bunを選べません。
次の条件をすべて満たすなら、Bun.Imageへ移行します。
- 少なくとも1つの層ですでにBun runtimeを使っている。
- 画像処理が、JPEG、PNG、WebP、AVIF、HEICの「デコード、リサイズ、再エンコード」である。
- Sharpのビルド済みバイナリの再ビルドでCIに苦しんでいる、またはAlpineコンテナを使い、libvipsのシステム依存に阻まれたことがある。
2026年5月時点で率直に評価すると、Bun.Imageは「Sharpの一般的な用途の95%」を満たします。そのうえインストール容量は大幅に小さく、ネイティブアドオン特有の手間もありません。残る5%では、Sharpが蓄えてきたlibvipsの厚みが今も価値を発揮します。そのため、一定期間は二重構成を前提にしてください。初日からあらゆる場所のSharpを外してはいけません。Bun.Imageの得意領域に合うルートから移し、合わないルートにはSharpを残し、任意角度の回転とアニメーションフレームが追加される可能性のあるBun 1.4.xで再評価します。
今後の更新で注目する機能は、任意角度の回転、アニメーションWebPのフレームアクセス、ICCの引き継ぎの3つです。Rustへの書き換えで開発が加速すれば、これらが次に実装される可能性は高いでしょう。Bunのchangelogを購読し、minorリリースごとに二重構成の分担を見直します。
本番へ出すSharp Bun移行コミットの完成形
次は、置き換え後の本番ルートです。エラー処理と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;明示的に固定しておくべき点は3つあります。
package.jsonの"engines": { "bun": ">=1.3.14" }は欠かせません。Bun.Imageが追加されたのは1.3.14です。それより前のバージョンでは実行時にBun.image is not a functionとなります。本番で500エラーにするのではなく、インストール時のエラーとして表面化させる必要があります。
@types/bunを置き換えるbun-typesパッケージには、1.3.14からBun.Imageの型定義が含まれます。@ts-expect-errorの回避コードなしでtsc --noEmitが通ります。エディターがまだBun.imageに赤い波線を表示するなら、bun-typesの固定バージョンが古すぎます。
ロールバック計画として、1回のリリースサイクルはsharpをoptionalDependenciesに残し、「注意点」の節で示した動的importの二重構成をフォールバック経路にします。本番指標が1週間正常だったことを確認したら、optionalDependenciesからsharpを外し、フォールバック分岐を削除します。この2つを同じコミットで行ってはいけません。慎重に進めるなら、同じ週にも行わないでください。
Bunの機能を本番で使えるか判断する基準は、changelogの主張ではありません。自分のコミットに入れられるかどうかです。これはそのまま出せます。
Bun.ImageはBun以外(Node.js)でも使えますか?
使えません。Bun.Imageはnpmパッケージではなく、ランタイムの組み込み機能です。NodeとBunの両方で動くSharp代替が必要なら、bun-image-turboのようなサードパーティ製パッケージを検討するか、Sharpを使い続けてください。
Sharpと完全互換のAPIですか、それとも参考にしただけですか?
チェーンの構造とメソッド名は、意図的にSharp互換になっています。コンストラクタ → .resize / .rotate / .flip / .modulate → .webp / .jpeg / .png / .avifという流れです。大半の呼び出し箇所はimportの行を替えるだけで移行できます。相違点は、任意角度の回転、アニメーションフレーム、ICCの引き継ぎ、.tile()の4つです。
Bun.Imageの内部では何を使っていますか?
JPEGのデコードとエンコードにはlibjpeg-turbo、PNGにはspng、WebPとAVIFにはlibwebpを使い、ジオメトリ処理にはBun独自のSIMDカーネル(i16固定小数点リサイズ)を採用しています。すべてBunバイナリへ組み込まれているため、ネイティブアドオンも再ビルドも不要です。
画像リサイズではBun.ImageとSharpのどちらが高速ですか?
一般的なJPEG/PNGのリサイズと再エンコードでは、ローカル環境における両者の差は約8%以内です。非常に大きい画像のストリーミング処理やアニメーションのワークロードでは、Sharpのlibvipsが今も高速です。Bun.Imageの大きな利点は画像1枚のCPU時間ではなく、インストール時間とメモリにあります。
今すぐBun.Imageへ移行すべきですか?
Bun runtimeを使い、画像パイプラインがJPEG/PNG/WebP/AVIFのデコード、リサイズ、再エンコードなら、移行して構いません。アニメーションWebPのフレーム、ICCカラープロファイルの保持、.tile()のDeep Zoomに依存する経路にはSharpを残し、二重構成で運用してください。
2026年9月5日







