Bun Image statt Sharp: Migration in 4 Schritten
Bun Image statt Sharp in 4 Schritten: Der Praxisleitfaden zeigt CI-Vorteile, kleinere Docker-Images und klare Grenzen bei ICC, WebP und Tiles.

Bun Image kam mit Bun v1.3.14 am 13. Mai 2026. Hinter Bun.Image steckt eine Pipeline aus libjpeg-turbo, spng und libwebp, die sich an der Sharp-API orientiert und ohne einen einzigen Build-Schritt für native Add-ons auskommt. Nach drei Jahren, in denen meine CI bei jedem Node-Update am libvips-Binärpaket von lovell/sharp scheiterte, war das die Version, mit der Sharp aus meinem Stack flog.
Warum Bun Image mit Version 1.3.14 Sharp ablöste
Die Release Notes zu Bun 1.3.14 erschienen am 13. Mai mit der üblichen Liste an Neuerungen. Zwischen „HTTP/3-Client“ und „7x schnelleren Warm-Installationen“ stand eher unscheinbar die Zeile, die meine Zeit mit Sharp beendete: Bun.Image, eine verkettbare, direkt in die Runtime integrierte Bildverarbeitung. libjpeg-turbo, spng und libwebp sind dafür unmittelbar in das Bun-Binärprogramm kompiliert.
Wer noch nie einen CI-Tag an Sharp verloren hat, kann diesen Absatz überspringen. Allen anderen ist das Drehbuch vertraut: sharp/lib/sharp-linuxmusl-x64.node wird nicht gefunden. Im Alpine-Container erscheint Cannot find module '../build/Release/sharp.node'. Der Docker-Layer-Cache wird ungültig, weil jemand die Node-Version angehoben hat und nun bei jedem Push npm rebuild sharp von vorn läuft. Der Vercel-Build greift auf das CDN für vorgefertigte Binärdateien zu und läuft in ein Timeout. Drei Jahre lang, in drei verschiedenen Projekten – und irgendwann schrieb ich apk add --no-cache vips-dev aus dem Muskelgedächtnis.
Bun.Image ist die runtime-native Antwort darauf. Für die Bildverarbeitung entfällt npm install, denn die Codecs stecken bereits im Bun-Binärprogramm. Ein natives Add-on, das nach einem Wechsel der Node-ABI neu gebaut werden müsste, gibt es ebenfalls nicht – schließlich gibt es weder Node noch ein Add-on. Die Geometrie-Kernel nutzen i16-Festkomma-SIMD; beim JPEG-Decoding wird automatisch auf die kleinste noch ausreichende Größe skaliert. Strukturell ist das genau die Lösung, die Sharp sein könnte, wenn Sharp nicht als Node-Add-on funktionieren müsste.
Noch aus einem anderen Grund war Version 1.3.14 für mich wichtig: Sie ist das letzte Zig-Release vor der von Anthropic finanzierten Neuentwicklung in Rust. The Register berichtete am 14. Mai über die Merge-Frequenz; von hier an ist mit erheblichem Entwicklungstempo zu rechnen. Mir ist es lieber, jetzt auf eine Bun-native Funktion umzusteigen, als eine Abhängigkeit von einem nativen Add-on durch den Umbau der Runtime mitzuschleppen.
Das ist kein Nachruf auf Sharp. Bei animiertem WebP, farbprofilkritischer Fotobearbeitung und der Deepzoom-Pyramide von tile() bleibt Sharp auf Basis von libvips die schnellste Lösung. Dieses Playbook gilt für die übrigen 95% der Bildverarbeitung – also für die Decode-Resize-Encode-Pipeline, die in den meisten Produktivsystemen läuft.
Sharp durch Bun Image ersetzen: die Migration in 4 Schritten
Den Wechsel habe ich in omidsaffari-admin vorgenommen. Dieser Worker bereitet die von gpt-image-2 erzeugten Titelbilder auf, bevor sie in R2 landen. In einer Datei gab es acht Sharp-Aufrufstellen, alle in dem Pfad nach dem PublishWorkflow-Schritt cover. Die gesamte Migration dauerte 42 Minuten – einschließlich des doppelten Encodings als WebP mit JPEG-Fallback.
Schritt 1 – Sharp-Einsatzstellen erfassen. Vor jeder Änderung müssen zunächst sämtliche Imports gefunden werden:
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/So ist von Anfang an klar, ob vier oder vierzig Aufrufstellen umzustellen sind. Bei vierzig erfolgt die Migration Route für Route und nicht in einem einzigen Durchgang.
Schritt 2 – Den Import durch Bun.file().image() ersetzen. Der Sharp-Konstruktor akzeptiert einen Pfad, einen Buffer oder einen Stream. Der Konstruktor von Bun.Image nimmt einen Pfad über Bun.file(), ein Uint8Array, ein Blob oder alles entgegen, was die Datei-Primitiven von Bun zurückgeben. Dazu zählen auch Bun.s3()-Referenzen, was die Struktur meines Codes verändert hat.
Schritt 3 – Die Methodenkette übertragen. Hier erfüllt Bun.Image das Versprechen einer „Sharp-kompatiblen“ API. Alle Methoden aus meinem Produktivcode ließen sich 1:1 abbilden: .resize(w, h, { fit: "cover" }) verhält sich identisch, .rotate(90) funktioniert mit der unten beschriebenen Einschränkung, .flip() und .flop() bleiben gleich, ebenso .modulate({ brightness, saturation }). Auch sämtliche abschließenden Formatmethoden sind vorhanden: .webp({ quality }), .jpeg({ quality }), .png(), .avif() und .heic().
Schritt 4 – Den Abschluss der Pipeline austauschen. Aus .toBuffer() von Sharp wird .toBuffer() von Bun.Image. Die Methode liefert allerdings ein Uint8Array statt eines Buffer zurück – wichtig, wenn der nächste Empfänger den Typ Buffer ausdrücklich prüft. .toFile(path) von Sharp wird zu .write(path). Auch das Prinzip der verzögerten Ausführung bleibt gleich: Erst das Warten auf den Abschluss startet die Verarbeitung.
Hier ist der tatsächliche Diff aus einem meiner Route-Handler:
// 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;
}Für den üblichen Anwendungsfall ist das bereits der gesamte Tausch: eine Importzeile und ein Konstruktoraufruf.
Nach der Migration bleibt die Ausgabe von bun pm ls | grep sharp leer. Im CI-Dockerfile entfällt die Zeile RUN apk add --no-cache vips-dev. Das resultierende Image ist ~80MB kleiner. Gleichzeitig schrumpft die package.json um eine Abhängigkeit und eine Peer-Dependency-Warnung.
Drei Fälle, die sich noch nicht sauber übertragen lassen
Diese Punkte sollten vor dem Entfernen von Sharp ehrlich geprüft werden. Sobald eine Pipeline mehr als reines Skalieren und Encodieren erledigt, dürfte mindestens einer davon relevant werden.
Fallstrick 1 – Durchreichen von ICC-Farbprofilen. Mit .withMetadata({ icc: "p3" }) bewahrt Sharp das Farbprofil des Eingangs beim Encodieren. Bun.Image entfernt ICC mit Stand 1.3.14. Bei Workflows mit sRGB als Ein- und Ausgabe – also bei den meisten Webbildern – bleibt das unsichtbar. In Fotopipelines, in denen hochgeladene Bilder im großen Display-P3-Farbraum erhalten bleiben sollen, liegt Sharp weiterhin vorn. Falls Bun.Image zwingend eingesetzt werden muss, lautet der Umweg: ICC-Block mit exifr auslesen, das Bild encodieren und den Block anschließend manuell wieder anhängen. Elegant ist das nicht.
Fallstrick 2 – Frames in animiertem WebP und GIF. Bun.Image decodiert bei einer animierten Eingabe nur den ersten Frame und verwirft den Rest. Für Sharps { animated: true } samt Zugriff auf einzelne Frames gibt es kein Gegenstück. Bei der Verarbeitung von Sprite-Sheets, der Erstellung animierter Vorschaubilder oder jedem anderen Frame-basierten Ablauf ist hier Schluss. Für diese Codepfade bleibt Sharp erforderlich.
Fallstrick 3 – Die Pyramide von .tile(). Sharp übernimmt von libvips die Deepzoom-/IIIF-Kachelerzeugung. Für einen Bildserver im Stil von Leaflet, eine Kartenkachel-Pipeline oder eine Zoom-Oberfläche auf Museumsniveau ist das unverzichtbar. Bun.Image besitzt keine Kachelfunktion und wird vermutlich noch eine Weile ohne auskommen müssen – in libvips stecken Jahrzehnte an Entwicklungsarbeit, und das Bun-Team dürfte zunächst die häufigsten Anwendungsfälle priorisieren.
Ein kleinerer Fallstrick gehört ebenfalls auf die Liste: .rotate(45) von Sharp dreht um beliebige Winkel und interpoliert bilinear. .rotate() von Bun.Image akzeptiert ausschließlich 90, 180 und 270. Für 99% der Titelbilder und Produktvorschaubilder spielt das keine Rolle. Bei Schräglagenkorrekturen oder bewusst schräg gestellten Motiven ist es dagegen ein Ausschlusskriterium.
Für Workloads mit einem dieser Sonderfälle setze ich inzwischen auf einen Dual-Stack: Bun.Image übernimmt den Standardpfad, Sharp läuft ausschließlich für die Problemfälle in einem Worker-Thread.
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();
}Der dynamische Import hält Sharp bei Deployment-Zielen aus dem Bundle heraus, die den langsameren Pfad nie benötigen.
Messwerte für CI und Kaltstart
Am wichtigsten ist für mich der Unterschied bei der Installationszeit, denn CI-Minuten summieren sich.
Auf meinem Ubuntu-x86_64-CI-Runner benötigte bun install mit festgesetzter Sharp-Version im Warmzustand 4.8s. Nach dem Entfernen von Sharp aus der package.json waren es 1.4s. Die Ersparnis entsteht, weil weder die vorgefertigte Sharp-Binärdatei heruntergeladen noch die optionale libvips-Systemabhängigkeit geprüft werden muss.
Eine Kaltinstallation ohne ~/.bun/install/cache und ohne node_modules verkürzte sich von 18.2s auf 7.1s. Eine einzige native Erweiterung aus einem Baum mit 200 Paketen zu entfernen, verändert die Installationszeit normalerweise nicht so stark. Hier fällt der Effekt größer aus, weil Sharps Postinstall-Skript der langsamste Einzelschritt im gesamten Baum war.
Bun 1.3.14 bringt außerdem den globalen Speicher des isolierten Linkers mit, den die Release Notes projektweit als „7x schnellere Warm-Installationen“ bezeichnen. Zusammen mit dem Entfernen von Sharp sank der vollständige warme bun install-Durchlauf in meinem Admin-Repository von 6.4s auf 1.1s. Das ist im lokalen Entwicklungsalltag unmittelbar spürbar: bun add some-package taugt nicht länger als Kaffeepause.
Nach dem Entfernen der vorgefertigten Sharp-Binärdatei und des vom Prebuild-Fallback verlangten Alpine-Pakets vips-dev wurden die Docker-Images ~80MB kleiner. Auch die Trefferquote des Layer-Caches in der CI steigt, weil der Layer unterhalb des Installationsschritts bei mehr Änderungen am Abhängigkeitsbaum stabil bleibt. Der Download der vorgefertigten Sharp-Binärdatei gehörte zu den lauteren Auslösern für Cache-Invalidierungen.
Generell halte ich meine Entwicklungswerkzeuge bewusst schlank – aus demselben Impuls heraus habe ich die Hooks von Claude Code 2.1.141 direkt am Veröffentlichungstag eingesetzt. Erst die Summe kleiner Verbesserungen an der Toolchain macht einen Ein-Personen-Betrieb konkurrenzfähig. Ein um 5 Sekunden schnelleres bun install klingt zunächst nicht spektakulär. Bei 80 Commits pro Woche sieht das anders aus.
Am meisten Sorge bereitete mir die Latenz pro Bild. Bei einer repräsentativen Skalierung – von 1024×1024 PNG auf 512×512 WebP mit Qualitätsstufe 82 – lagen Sharp 0.34.2 und Bun.Image auf meinem lokalen M2 höchstens 8% auseinander. Für einen Web-Workload ist keine der beiden Abweichungen relevant.
Dass Bun.Image beim reinen Skalieren mithalten kann, obwohl es erst seit achtzehn Monaten entwickelt wird und in libvips zwei Jahrzehnte stecken, hat architektonische Gründe: i16-Festkomma-SIMD-Kernel für die Skalierung und eine JPEG-IDCT-Skalierung, die schon beim Decodieren die kleinste ausreichende Größe wählt. Ein JPEG mit 4000×4000 wird nicht erst vollständig als Bitmap decodiert und anschließend verkleinert – Bun.Image decodiert es direkt in Zielauflösung. Sharp nutzt über libjpeg-turbo denselben Kniff, weshalb beide Pipelines in einer ähnlichen Größenordnung landen.
Beim Speicher setzt sich Bun.Image deutlicher ab: Durch das kopierfreie Ausleihen von ArrayBuffer bleibt der maximale RSS bei Stapeln ab 50+ Bildern unter dem von Sharp. Wer Galerien in einem einzigen Worker-Aufruf verarbeitet, profitiert davon. Bei genau einem Bild pro Request ist der Vorteil unsichtbar.
Wann Sharp gewinnt – und wann sich die Migration lohnt
Sharp sollte bleiben, sobald einer dieser Punkte zutrifft:
- Animierte WebP-Dateien müssen Frame für Frame zugänglich sein.
- ICC-Farbprofile für Fotografie mit großem Farbumfang müssen erhalten bleiben.
- Die Deepzoom-Pyramide von
.tile()ist für Bildserver oder Kartenkacheln erforderlich. - Beliebige Drehwinkel mit Interpolation werden benötigt.
- Das Deployment läuft ausschließlich auf Node-Zielen – etwa Vercel Node Functions, AWS Lambda mit Node-Runtime oder Cloudflare Workers, wo Bun noch nicht verfügbar ist.
Eine Migration zu Bun.Image passt, wenn alle folgenden Bedingungen erfüllt sind:
- In mindestens einer Schicht läuft bereits die Bun-Runtime.
- Die Bildverarbeitung besteht aus „Decodieren, Skalieren, neu Encodieren“ in JPEG, PNG, WebP, AVIF oder HEIC.
- Die CI leidet unter wiederholten Builds vorgefertigter Sharp-Binärdateien, oder Alpine-Container sind bereits an der libvips-Systemabhängigkeit gescheitert.
Die ehrliche Einschätzung im Mai 2026: Bun.Image deckt „95% des üblichen Sharp-Einsatzes“ ab, benötigt deutlich weniger Installationsballast und erspart jede Zeremonie rund um native Add-ons. In den übrigen 5% zahlt sich die Tiefe von Sharps libvips weiterhin aus. Daher gehört eine Dual-Stack-Phase in den Plan – Sharp sollte nicht am ersten Tag überall entfernt werden. Zuerst kommen die Routen an die Reihe, die im idealen Einsatzbereich von Bun.Image liegen. Sharp bleibt dort, wo das nicht gilt. Mit Bun 1.4.x kann die Aufteilung erneut bewertet werden, wenn voraussichtlich beliebige Drehwinkel und animierte Frames hinzukommen.
Auf der Beobachtungsliste für Upgrades stehen beliebige Drehwinkel, der Frame-Zugriff auf animiertes WebP und das Durchreichen von ICC. Angesichts des Entwicklungstempos der Rust-Neuentwicklung sind das die drei Funktionen, die am ehesten als Nächstes erscheinen dürften. Es lohnt sich, den Bun-Changelog zu abonnieren und die Dual-Stack-Aufteilung bei jeder Minor-Version erneut zu prüfen.
Der exakte Migrations-Commit für die Produktion
So sieht eine meiner Produktiv-Routen nach dem Wechsel aus – einschließlich Fehlerbehandlung und festgelegter Engine-Version:
// 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;Drei Punkte sollten ausdrücklich festgelegt werden:
Die Zeile "engines": { "bun": ">=1.3.14" } in der package.json ist unverzichtbar. Bun.Image kam mit Version 1.3.14 hinzu. Ältere Versionen brechen zur Laufzeit mit Bun.image is not a function ab – und dieser Fehler soll bei der Installation auffallen, nicht als 500 im Produktivbetrieb.
Das Paket bun-types, der Nachfolger von @types/bun, enthält die Bun.Image-Typdefinitionen ab Version 1.3.14. tsc --noEmit läuft ohne Hilfskonstruktionen mit @ts-expect-error durch. Falls der Editor Bun.image weiterhin rot unterkringelt, ist die festgesetzte Version von bun-types zu niedrig.
Rollback-Plan: sharp bleibt für einen Release-Zyklus in optionalDependencies; der dynamisch importierte Dual-Stack aus dem Abschnitt zu den Fallstricken dient als Rückfallpfad. Nach einer Woche mit unauffälligen Produktionsmetriken kann sharp aus optionalDependencies verschwinden und der Fallback-Zweig gelöscht werden. Beides gehört weder in denselben Commit noch – bei besonders vorsichtigem Vorgehen – in dieselbe Woche.
Ob ein Bun-Feature produktionsreif ist, entscheidet nicht die Behauptung im Changelog. Entscheidend ist, ob es in einem eigenen Commit landen würde. Dieses hier wird ausgeliefert.
Funktioniert Bun.Image außerhalb von Bun, also in Node.js?
Nein. Bun.Image ist fester Bestandteil der Runtime und kein npm-Paket. Wer eine portable Sharp-Alternative für Node und Bun benötigt, kann Drittanbieterpakete wie bun-image-turbo prüfen oder bei Sharp bleiben.
Ist die API wirklich ein Drop-in-Ersatz für Sharp oder nur davon inspiriert?
Die Struktur der Methodenkette und die Methodennamen sind bewusst Sharp-kompatibel: Konstruktor → .resize / .rotate / .flip / .modulate → Abschluss über .webp / .jpeg / .png / .avif. Bei den meisten Aufrufstellen genügt ein Tausch der Importzeile. Die vier Abweichungen sind beliebige Drehwinkel, animierte Frames, das Durchreichen von ICC und .tile().
Was verwendet Bun.Image intern?
libjpeg-turbo für das Decodieren und Encodieren von JPEG, spng für PNG, libwebp für WebP und AVIF sowie Buns eigene SIMD-Geometrie-Kernel mit i16-Festkommaskalierung. Alles ist in das Bun-Binärprogramm kompiliert – ohne natives Add-on und ohne erneuten Build-Schritt.
Wie schnell ist Bun.Image beim Skalieren im Vergleich zu Sharp?
Beim üblichen Skalieren und erneuten Encodieren von JPEG und PNG liegen beide auf lokaler Hardware höchstens ~8% auseinander. Bei sehr großen Bildern im Streamingbetrieb und bei animierten Workloads bleibt Sharps libvips schneller. Der größere Vorteil von Bun.Image liegt bei Installationszeit und Speicherbedarf, nicht bei der reinen CPU-Zeit einer einzelnen Skalierung.
Sollte die Migration jetzt erfolgen?
Ja, wenn die Bun-Runtime bereits läuft und die Bildpipeline JPEG, PNG, WebP oder AVIF lediglich decodiert, skaliert und neu encodiert. Wer auf animierte WebP-Frames, den Erhalt von ICC-Farbprofilen oder .tile() für Deepzoom angewiesen ist, behält Sharp auf diesen Pfaden und setzt auf einen Dual-Stack.
5. Sept. 2026







