Bun Image : migrer depuis Sharp en 4 étapes avec Bun 1.3.14
Passez de Sharp à Bun Image en quatre étapes : API, limites, performances CI et stratégie de repli pour migrer vos traitements d’images sans surprise.

Bun a livré la v1.3.14 le 13 mai 2026 avec Bun Image, un pipeline fondé sur libjpeg-turbo + spng + libwebp qui reprend l’API de Sharp et fonctionne sans la moindre étape de compilation d’un addon natif. Après trois ans à voir la CI échouer sur le binaire libvips de lovell/sharp à chaque mise à jour de Node, c’est la version qui m’a décidé à retirer Sharp.
Pourquoi j’ai abandonné Sharp pour Bun Image dès la sortie de la 1.3.14
Les notes de version de Bun 1.3.14 sont arrivées le 13 mai avec leur liste habituelle de nouveautés. Entre « client HTTP/3 » et « installations à chaud 7x plus rapides » se cachait la ligne qui a mis fin à mon usage de Sharp : Bun.Image, un pipeline d’images chaînable intégré au runtime, avec libjpeg-turbo, spng et libwebp compilés directement dans le binaire Bun.
Si Sharp ne vous a jamais fait perdre une journée de CI, vous pouvez passer ce paragraphe. Sinon, le scénario est familier : sharp/lib/sharp-linuxmusl-x64.node introuvable ; Cannot find module '../build/Release/sharp.node' dans le conteneur Alpine ; le cache des layers Docker invalidé après une mise à jour de Node, puis npm rebuild sharp relancé de zéro à chaque push ; ou encore le build Vercel qui tente de joindre le CDN des binaires précompilés avant d’expirer. Trois ans, trois projets distincts, et à un moment j’ai fini par taper apk add --no-cache vips-dev par réflexe.
Bun.Image apporte la réponse native du runtime. Aucun npm install dédié au traitement d’images : les codecs sont inclus dans le binaire Bun. Aucun addon natif à reconstruire quand l’ABI de Node change, puisqu’il n’y a ni Node ni addon. Les kernels géométriques reposent sur du SIMD en virgule fixe i16, et le décodage JPEG se cale automatiquement sur la plus petite taille suffisante. Dans sa conception, c’est ce que Sharp aurait pu être s’il n’avait pas dû rester un addon Node.
La 1.3.14 comptait aussi pour une autre raison : c’est la dernière version en Zig avant l’arrivée de la réécriture en Rust financée par Anthropic. The Register a décrit le rythme des merges le 14 mai, et la cadence d’ingénierie devrait sérieusement accélérer. Je préfère migrer dès maintenant vers une primitive Bun plutôt que de conserver une dépendance à un addon natif pendant la réécriture d’un runtime.
Sharp n’est pas mort pour autant. Avec libvips, il reste le champion de la vitesse pour le WebP animé, les workflows photo où les profils colorimétriques sont critiques et la pyramide deepzoom de tile(). Ce guide couvre les 95% restants : le pipeline décodage-redimensionnement-encodage sur lequel reposent la plupart des applications en production.
Migrer de Sharp vers Bun Image en 4 étapes
J’ai effectué cette migration dans omidsaffari-admin, le worker qui finalise les images de couverture produites par gpt-image-2 avant leur envoi vers R2. Le fichier comptait huit appels à Sharp, tous situés sur le chemin exécuté après l’étape cover de PublishWorkflow. Temps total : 42 minutes, y compris pour le double encodage WebP avec fallback JPEG.
Étape 1 – Inventorier tous les usages de Sharp. Avant de modifier quoi que ce soit, retrouvez chaque import :
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/Il faut savoir dès le départ si la migration porte sur quatre appels ou quarante. Dans le second cas, procédez route par route, pas en une seule fois.
Étape 2 – Remplacer l’import par Bun.file().image(). Le constructeur de Sharp accepte un chemin, un Buffer ou un Stream. Celui de Bun.Image reçoit un chemin via Bun.file(), un Uint8Array, un Blob ou tout résultat renvoyé par les primitives de fichiers de Bun — références Bun.s3() comprises, ce qui a changé la structure de mon code.
Étape 3 – Transposer la chaîne d’appels. C’est ici que la compatibilité annoncée avec Sharp prend tout son sens. Toutes les méthodes présentes dans mon code de production avaient une correspondance 1:1 : .resize(w, h, { fit: "cover" }) est identique ; .rotate(90) fonctionne, sous réserve de la limite décrite plus bas ; .flip() et .flop() ne changent pas ; .modulate({ brightness, saturation }) non plus. Tous les formats de sortie sont là : .webp({ quality }), .jpeg({ quality }), .png(), .avif() et .heic().
Étape 4 – Changer l’appel terminal. Le .toBuffer() de Sharp devient le .toBuffer() de Bun.Image. Celui-ci renvoie un Uint8Array, et non un Buffer — un détail important si la destination vérifie explicitement le type Buffer. Le .toFile(path) de Sharp devient .write(path). Le principe du pipeline paresseux reste le même : rien ne s’exécute tant que le résultat de l’appel terminal n’est pas attendu.
Voici le diff réel de l’un de mes handlers de route :
// 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;
}Pour le cas courant, toute la migration tient là : une ligne d’import et un appel de constructeur.
Après la migration, bun pm ls | grep sharp ne renvoie plus rien. La ligne RUN apk add --no-cache vips-dev disparaît du Dockerfile de CI. L’image obtenue perd ~80MB. Quant au package.json, il compte une dépendance et un avertissement de peer dependency en moins.
Trois limites qui n’ont pas encore d’équivalent direct
Mieux vaut les connaître avant de supprimer Sharp, car au moins l’une d’elles posera problème si votre pipeline ne se résume pas au redimensionnement et à l’encodage.
Piège 1 – Conservation du profil colorimétrique ICC. Le .withMetadata({ icc: "p3" }) de Sharp préserve le profil colorimétrique de l’image source lors de l’encodage. En 1.3.14, Bun.Image supprime les données ICC. Pour les workflows sRGB en entrée comme en sortie — soit la majorité des images web — cela ne se voit pas. Pour un pipeline photo qui doit conserver le gamut étendu Display-P3 d’un fichier envoyé par un utilisateur, Sharp garde l’avantage. Si Bun.Image est impératif, il reste une solution : lire le bloc ICC avec exifr, encoder l’image, puis le rattacher manuellement. Ce n’est pas élégant.
Piège 2 – Frames des WebP et GIF animés. Bun.Image ne décode que la première frame d’une image animée et ignore toutes les autres. Il n’existe aucun équivalent au { animated: true } de Sharp ni à son accès frame par frame. Pour traiter des sprite sheets, générer des miniatures animées ou parcourir les frames de quelque manière que ce soit, la limite est absolue. Conservez Sharp sur ces chemins de code.
Piège 3 – La pyramide .tile(). Sharp hérite de libvips pour générer des tuiles deepzoom / IIIF. Si vous exploitez un serveur d’images de type Leaflet, un pipeline de tuiles cartographiques ou une interface de zoom digne d’un musée, cette fonction est indispensable. Bun.Image ne propose aucune primitive de tuilage et cela ne devrait pas changer tout de suite : libvips concentre des décennies de travail, tandis que l’équipe Bun donnera d’abord la priorité aux usages courants.
Une limite plus modeste mérite aussi d’être signalée : .rotate(45) dans Sharp accepte un angle quelconque avec interpolation bilinéaire. Avec Bun.Image, .rotate() se limite à 90, 180 et 270. Pour 99% des traitements de couvertures et de miniatures produit, cela ne change rien. En revanche, c’est bloquant pour corriger une inclinaison ou créer un effet penché.
Pour les workloads concernés par l’un de ces cas, j’utilise désormais une architecture à deux moteurs : Bun.Image sur le chemin courant, et Sharp épinglé dans un thread de worker réservé aux exceptions.
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();
}Grâce à l’import dynamique, Sharp reste hors du bundle pour les cibles de déploiement qui n’empruntent jamais ce chemin plus lent.
Les chiffres en CI et au démarrage à froid
Le gain sur le temps d’installation est celui qui m’importe le plus, car les minutes de CI s’accumulent vite.
Sur mon runner CI Ubuntu x86_64, un bun install à chaud prenait 4.8s avec Sharp épinglé. Après son retrait du package.json, il ne demande plus que 1.4s. L’écart vient de la suppression du téléchargement du binaire précompilé de Sharp et de la vérification de la dépendance système optionnelle à libvips.
À froid, sans ~/.bun/install/cache ni node_modules, le temps d’installation est passé de 18.2s à 7.1s. Retirer un addon natif d’un arbre de 200 packages produit rarement un tel effet : ici, la baisse est disproportionnée parce que le postinstall de Sharp était l’étape la plus lente de tout l’arbre.
Bun 1.3.14 livre également le store global de son linker isolé, présenté dans les notes de version comme la source d’« installations à chaud 7x plus rapides » à l’échelle d’un projet. Avec le retrait de Sharp, le cycle complet de bun install à chaud sur le dépôt d’administration est tombé de 6.4s à 1.1s. En développement local, la différence se ressent immédiatement : bun add some-package ne laisse plus le temps d’aller chercher un café.
Les images Docker ont perdu environ 80MB après le retrait du binaire précompilé de Sharp et du package Alpine vips-dev exigé par le fallback. Le cache de layers fonctionne mieux en CI, car le layer sous l’étape d’installation reste stable malgré davantage de changements de dépendances ; le téléchargement du binaire précompilé de Sharp comptait parmi les causes d’invalidation les plus fréquentes.
Je veille de manière générale à garder une chaîne d’outils légère. C’est ce même réflexe qui m’a poussé à déployer les hooks de Claude Code 2.1.141 le jour de leur sortie. Pour une structure indépendante, c’est l’accumulation de ces petits gains qui fait la différence. Un bun install accéléré de 5 secondes paraît anodin. Multipliez-le par 80 commits par semaine.
La latence par image était le chiffre qui m’inquiétait le plus. Sur un redimensionnement représentatif — PNG 1024×1024 vers WebP 512×512 en qualité 82 — Sharp 0.34.2 et Bun.Image sont arrivés à moins de 8% d’écart sur mon M2 local. Pour une charge web, cet écart n’a aucune importance.
Si Bun.Image rivalise sur le redimensionnement brut malgré ses dix-huit mois d’existence face aux deux décennies de libvips, c’est grâce à ses kernels de redimensionnement SIMD en virgule fixe i16 et à la mise à l’échelle IDCT des JPEG vers la plus petite taille suffisante pendant le décodage. Au lieu de décoder un JPEG 4000×4000 en bitmap complet avant de le réduire, Bun.Image le décode directement à la résolution cible. Sharp applique la même technique via libjpeg-turbo, ce qui explique leurs performances voisines.
C’est sur la mémoire que Bun.Image prend une avance perceptible : l’emprunt sans copie des ArrayBuffer réduit le pic de RSS par rapport à Sharp sur des lots de 50+ images. Le gain compte lorsqu’une seule invocation de worker traite des galeries entières. À raison d’une image par requête, il passe inaperçu.
Dans quels cas Sharp reste meilleur, et quand faut-il migrer vers Bun Image ?
Conservez Sharp dans les cas suivants :
- Vous devez accéder aux frames d’un WebP animé une par une.
- Vous préservez les profils colorimétriques ICC pour des photos à gamut étendu.
- Votre serveur d’images ou votre pipeline de tuiles cartographiques dépend de la pyramide deepzoom
.tile(). - Vous avez besoin d’une rotation selon un angle quelconque avec interpolation.
- Vos cibles de déploiement sont exclusivement sous Node — fonctions Vercel Node, runtime Node d’AWS Lambda, Cloudflare Workers, où Bun n’est pas encore disponible — et Bun n’est donc pas envisageable.
Migrez vers Bun.Image si toutes les conditions suivantes sont réunies :
- Au moins une couche de votre stack utilise déjà le runtime Bun.
- Vos traitements consistent à décoder, redimensionner puis réencoder des images JPEG, PNG, WebP, AVIF ou HEIC.
- Votre CI souffre des reconstructions de binaires précompilés de Sharp, ou vos conteneurs Alpine se heurtent à la dépendance système libvips.
Le constat honnête en mai 2026 : Bun.Image couvre « 95% des usages courants de Sharp », avec une empreinte d’installation bien plus faible et sans aucune contrainte liée aux addons natifs. Les 5% restants sont précisément ceux où la maturité de libvips justifie encore Sharp. Il faut donc prévoir une période à deux moteurs plutôt que tout supprimer dès le premier jour. Migrez les routes qui correspondent au terrain idéal de Bun.Image, conservez Sharp pour les autres, puis refaites le point avec Bun 1.4.x, lorsque la rotation libre et les frames animées devraient probablement arriver.
Trois évolutions sont à surveiller : la rotation libre, l’accès aux frames WebP animées et la conservation des profils ICC. Au vu de la cadence permise par la réécriture en Rust, ce sont les fonctions les plus susceptibles d’arriver ensuite. Abonnez-vous au changelog de Bun et réévaluez la répartition entre les deux moteurs à chaque version mineure.
Le commit de migration exact que je mettrais en production
Voici une route de production après la migration, avec la gestion des erreurs et la version minimale du moteur :
// 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;Trois éléments doivent être épinglés explicitement :
Dans le package.json, la ligne "engines": { "bun": ">=1.3.14" } est indispensable. Bun.Image est apparu en 1.3.14 : les versions antérieures échouent à l’exécution avec Bun.image is not a function. Mieux vaut le découvrir lors de l’installation que par une erreur 500 en production.
Le package bun-types, qui remplace @types/bun, inclut les types de Bun.Image à partir de la 1.3.14. tsc --noEmit passe sans shim @ts-expect-error. Si votre éditeur souligne encore Bun.image en rouge, la version épinglée de bun-types est trop ancienne.
Plan de rollback : conservez sharp dans optionalDependencies pendant un cycle de release, avec l’architecture à deux moteurs et l’import dynamique de la section sur les limites comme solution de repli. Après une semaine de métriques au vert en production, retirez sharp d’optionalDependencies et supprimez la branche de fallback. Ne faites pas les deux dans le même commit. Et, si vous êtes prudent, pas la même semaine non plus.
Le vrai test pour savoir si une fonction de Bun est prête pour la production ne tient pas à une promesse du changelog. La question est simple : l’intégreriez-vous à votre propre commit ? Pour celle-ci, ma réponse est oui.
Bun.Image fonctionne-t-il en dehors de Bun, avec Node.js ?
Non. Bun.Image est intégré au runtime ; ce n’est pas un package npm. Pour disposer d’une alternative à Sharp portable entre Node et Bun, tournez-vous vers des packages tiers comme bun-image-turbo, ou restez sur Sharp.
L’API remplace-t-elle vraiment Sharp à l’identique, ou s’en inspire-t-elle seulement ?
La structure de la chaîne et les noms de méthodes sont volontairement compatibles avec Sharp : constructeur → .resize / .rotate / .flip / .modulate → terminal .webp / .jpeg / .png / .avif. Pour la plupart des appels, seule la ligne d’import change. Les quatre différences concernent la rotation libre, les frames animées, la conservation des profils ICC et .tile().
Qu’utilise Bun.Image en interne ?
libjpeg-turbo pour décoder et encoder le JPEG, spng pour le PNG, libwebp pour le WebP et l’AVIF, ainsi que les kernels géométriques SIMD de Bun pour le redimensionnement en virgule fixe i16. Tout est compilé dans le binaire Bun : aucun addon natif, aucune étape de reconstruction.
Quelles performances offre Bun.Image face à Sharp pour le redimensionnement ?
Sur les opérations courantes de redimensionnement et de réencodage JPEG/PNG, les deux restent à ~8% l’un de l’autre sur une machine locale. Grâce à libvips, Sharp demeure plus rapide pour streamer de très grandes images et traiter les formats animés. Le principal avantage de Bun.Image concerne le temps d’installation et la mémoire, pas le CPU brut sur un redimensionnement isolé.
Faut-il migrer maintenant ?
Oui, si votre runtime est Bun et que votre pipeline décode, redimensionne et réencode des fichiers JPEG, PNG, WebP ou AVIF. Si vous dépendez des frames WebP animées, de la conservation des profils colorimétriques ICC ou du deepzoom .tile(), gardez Sharp sur ces chemins et utilisez les deux moteurs.
6 sept. 2026







