Bun Image al posto di Sharp: migrazione in 4 passaggi
Scopri come migrare da Sharp a Bun Image in 4 passaggi, ridurre i tempi della CI e il peso delle immagini Docker, mantenendo libvips nei casi avanzati.

Il 13 maggio 2026 Bun ha pubblicato la v1.3.14 con Bun Image, una pipeline basata su libjpeg-turbo + spng + libwebp che riprende l’API di Sharp e funziona senza neppure un passaggio di build per addon nativi. Dopo tre anni di CI mandata in errore dal binario libvips di lovell/sharp ogni volta che aggiornavo Node, è stata questa la versione che mi ha convinto a eliminare Sharp.
Perché ho abbandonato Sharp appena è arrivato Bun Image 1.3.14
Le note di rilascio di Bun 1.3.14 sono arrivate il 13 maggio con il consueto elenco puntato. Nascosta tra “HTTP/3 client” e “7x faster warm installs” c’era la riga che ha chiuso la mia esperienza con Sharp: Bun.Image, una pipeline di elaborazione concatenabile integrata nel runtime, con libjpeg-turbo, spng e libwebp compilati direttamente nel binario di Bun.
Se Sharp non ti ha mai fatto perdere una giornata di CI, puoi saltare questo paragrafo. In caso contrario, il copione è noto: sharp/lib/sharp-linuxmusl-x64.node non trovato; Cannot find module '../build/Release/sharp.node' nel container Alpine; la cache dei layer Docker invalidata perché qualcuno ha aggiornato Node e ora npm rebuild sharp riparte da zero a ogni push; la build Vercel che tenta di raggiungere la CDN dei binari precompilati e va in timeout. Tre anni così, su tre progetti distinti, e a un certo punto ho iniziato a scrivere apk add --no-cache vips-dev in automatico.
Bun.Image è la risposta nativa del runtime. Per elaborare le immagini non serve alcun passaggio npm install: i codec sono già nel binario di Bun. Non esistono addon nativi da ricompilare quando cambia l’ABI di Node, perché non ci sono né Node né addon. I kernel geometrici usano SIMD fixed-point i16; durante la decodifica, i JPEG vengono ridimensionati automaticamente alla misura minima sufficiente. A livello strutturale, è ciò che Sharp sarebbe potuto essere se non avesse dovuto funzionare come addon di Node.
C’è poi un altro motivo per cui la 1.3.14 ha contato: è l’ultima release in Zig prima dell’arrivo della riscrittura in Rust finanziata da Anthropic. Il 14 maggio The Register ha raccontato il ritmo dei merge, e da qui in avanti la velocità di sviluppo sarà notevole. Preferisco migrare subito verso una primitiva di Bun, invece di trascinarmi una dipendenza basata su addon nativi durante la riscrittura del runtime.
Questo non è il necrologio di Sharp. Sharp su libvips resta il riferimento per WebP animati, lavori fotografici in cui i profili colore sono cruciali e piramidi deep zoom generate con tile(). Quello che segue è il playbook per il 95% dell’elaborazione immagini che non ricade in questi tre casi: la pipeline decodifica-ridimensiona-codifica usata dalla maggior parte delle applicazioni in produzione.
La migrazione in 4 passaggi della mia pipeline per le cover
Ho eseguito la sostituzione in omidsaffari-admin, il worker che elabora l’output delle cover di gpt-image-2 prima di inviarlo a R2. In un solo file c’erano otto punti in cui veniva chiamato Sharp, tutti nel percorso avviato dopo lo step cover di PublishWorkflow. Tempo totale della migrazione: 42 minuti, compreso il ramo con doppia codifica WebP e fallback JPEG.
Passaggio 1 – Fai l’inventario di dove usi Sharp. Prima di modificare il codice, trova ogni import:
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/Conviene sapere subito se la sostituzione riguarda quattro punti o quaranta. Se sono quaranta, procedi una route alla volta, non tutte insieme.
Passaggio 2 – Sostituisci l’import con Bun.file().image(). Il costruttore di Sharp accetta un percorso, un Buffer o uno Stream. Quello di Bun.Image accetta un percorso tramite Bun.file(), un Uint8Array, un Blob o qualsiasi valore restituito dalle primitive file di Bun, compresi i riferimenti Bun.s3(), che nel mio caso hanno cambiato la struttura del codice.
Passaggio 3 – Mappa la catena. È qui che si capisce perché Bun.Image viene presentato come compatibile con Sharp. Tutti i metodi che usavo in produzione avevano una corrispondenza 1:1: .resize(w, h, { fit: "cover" }) è identico, .rotate(90) funziona (con il limite sulle rotazioni spiegato più avanti), .flip() e .flop() restano uguali, così come .modulate({ brightness, saturation }). Sono disponibili anche tutti i metodi terminali per i formati: .webp({ quality }), .jpeg({ quality }), .png(), .avif() e .heic().
Passaggio 4 – Sostituisci il metodo terminale. Il .toBuffer() di Sharp diventa il .toBuffer() di Bun.Image (restituisce Uint8Array, non Buffer: una distinzione importante se il passaggio successivo verifica esplicitamente il tipo Buffer). Il .toFile(path) di Sharp diventa .write(path). Il contratto della pipeline lazy resta identico: l’elaborazione non parte finché non esegui l’await del metodo terminale.
Ecco il diff reale di uno dei 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;
}Per il caso più comune, la sostituzione finisce qui: una riga di import e una chiamata al costruttore.
Dopo la migrazione, bun pm ls | grep sharp non restituisce nulla. Dal Dockerfile della CI sparisce la riga RUN apk add --no-cache vips-dev. L’immagine risultante pesa ~80MB in meno. E dal package.json scompaiono una dipendenza e un avviso sulle peer dependency.
Tre aspetti che non hanno ancora una corrispondenza diretta
Prima di iniziare a rimuovere Sharp, conviene essere realistici su questi limiti: se la pipeline non si limita a ridimensionare e codificare, è probabile che almeno uno diventi un problema.
Limite 1 – Mantenimento del profilo colore ICC. Con .withMetadata({ icc: "p3" }), Sharp conserva il profilo colore dell’input anche dopo la codifica. Alla versione 1.3.14, Bun.Image elimina i profili ICC. Nei flussi sRGB in ingresso e sRGB in uscita, cioè nella maggior parte dei casi sul web, la differenza non si vede. Se invece una pipeline fotografica deve preservare un’immagine Display-P3 ad ampia gamma caricata dall’utente, Sharp resta la scelta migliore. Se devi comunque usare Bun.Image, l’alternativa è leggere il blocco ICC con exifr, codificare e poi riagganciarlo manualmente. Non è una soluzione elegante.
Limite 2 – Frame di WebP animati e GIF. Bun.Image decodifica il primo frame di un input animato e scarta tutti gli altri. Non esiste un equivalente di { animated: true } di Sharp con accesso ai singoli frame. Per elaborare sprite sheet, generare thumbnail animate o svolgere qualsiasi operazione frame per frame, il limite è invalicabile: mantieni Sharp in quei percorsi.
Limite 3 – La piramide .tile(). Sharp eredita da libvips la generazione di tile deep zoom / IIIF. Se gestisci un server di immagini in stile Leaflet, una pipeline di tile cartografiche o un’interfaccia di zoom per collezioni museali, non puoi farne a meno. Bun.Image non offre una primitiva per i tile e probabilmente non la offrirà ancora per un po’: libvips racchiude decenni di lavoro, mentre il team di Bun darà priorità ai casi d’uso più comuni.
Un limite minore che vale la pena segnalare: .rotate(45) di Sharp supporta rotazioni di qualsiasi grado con interpolazione bilineare. .rotate() di Bun.Image accetta soltanto 90, 180 e 270. Nel 99% dei flussi per cover e thumbnail di prodotto non cambia nulla. Per correggere immagini inclinate o applicare effetti estetici di rotazione, invece, è un blocco.
Per i carichi che ricadono in uno dei casi precedenti, ora uso un approccio dual stack: Bun.Image nel percorso comune e Sharp, fissato in un worker thread, soltanto per i casi problematici.
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();
}L’import dinamico tiene Sharp fuori dal bundle per i target di deploy che non attraversano mai il percorso lento.
I numeri di CI e cold start
La differenza nei tempi di installazione è il dato che conta di più, perché i minuti di CI si sommano.
Sul mio runner CI Ubuntu x86_64, bun install con Sharp fissato a una versione richiedeva 4.8s a caldo. Dopo aver rimosso Sharp dal package.json, il tempo è sceso a 1.4s. Il risparmio deriva dal fatto che non vengono più scaricati i binari precompilati di Sharp né eseguito il controllo della dipendenza opzionale di sistema libvips.
L’installazione a freddo (senza ~/.bun/install/cache e senza node_modules) è passata da 18.2s a 7.1s. In genere, eliminare un singolo addon nativo da un albero di 200 pacchetti non produce una differenza simile; qui il calo è così marcato perché il postinstall di Sharp era il passaggio più lento dell’intero albero.
Bun 1.3.14 include anche lo store globale del linker isolato, descritto nelle note di rilascio come “7x faster warm installs” sull’intero progetto. Sommando questa novità alla rimozione di Sharp, nel repository admin il mio ciclo completo di bun install a caldo è passato da 6.4s a 1.1s. È il genere di miglioramento che si percepisce nello sviluppo locale: bun add some-package smette di essere una pausa caffè.
Dopo aver rimosso il binario precompilato di Sharp e il pacchetto Alpine vips-dev richiesto dal fallback della prebuild, le immagini Docker si sono ridotte di circa 80MB. In CI aumentano anche gli hit della cache dei layer, perché quello sotto lo step di installazione resta stabile nonostante più variazioni nelle dipendenze: il download del binario precompilato di Sharp era una delle cause più frequenti di invalidazione della cache.
In generale cerco di mantenere essenziale la toolchain di sviluppo: è lo stesso impulso che mi ha portato a pubblicare gli hook di Claude Code 2.1.141 il giorno stesso del rilascio. È l’effetto cumulativo dei piccoli miglioramenti alla toolchain a rendere competitivo un progetto individuale. Un bun install più veloce di 5 secondi sembra poca cosa; moltiplicalo per 80 commit alla settimana.
La latenza per immagine era il dato che mi preoccupava di più. In un ridimensionamento rappresentativo — da PNG 1024×1024 a WebP 512×512 con qualità 82 — Sharp 0.34.2 e Bun.Image sono rimasti entro l’8% l’uno dall’altro sul mio M2. Per un carico web, nessuna delle due differenze è rilevante.
Il motivo architetturale per cui Bun.Image è competitivo nel puro ridimensionamento, nonostante abbia diciotto mesi contro i due decenni di libvips, sta nei kernel SIMD fixed-point i16 e nel ridimensionamento IDCT dei JPEG alla misura minima sufficiente durante la decodifica. Non viene decodificato un JPEG 4000×4000 in una bitmap completa per poi ridimensionarlo: Bun.Image lo decodifica direttamente alla risoluzione di destinazione. Sharp usa lo stesso accorgimento tramite libjpeg-turbo, ed è per questo che le due pipeline finiscono così vicine.
È sulla memoria che Bun.Image passa nettamente in vantaggio: il borrowing zero-copy degli ArrayBuffer riduce il picco RSS nei batch di 50+ immagini rispetto a Sharp. La differenza conta se elabori intere gallerie nella stessa invocazione del worker; con una sola immagine per richiesta, resta invisibile.
Quando conviene ancora Sharp e quando migrare
Mantieni Sharp se si verifica anche solo uno di questi casi:
- Ti serve accedere frame per frame a un WebP animato.
- Devi conservare i profili colore ICC per fotografie ad ampia gamma.
- Dipendi dalla piramide deep zoom
.tile()per server di immagini o pipeline di tile cartografiche. - Ti servono rotazioni ad angolo libero con interpolazione.
- Il deploy avviene esclusivamente su target Node — funzioni Node di Vercel, runtime Node di AWS Lambda, Cloudflare Workers (Bun non è ancora disponibile) — dove Bun non è un’opzione.
Migra a Bun.Image se valgono tutte queste condizioni:
- Usi già il runtime Bun in almeno un livello dell’architettura.
- L’elaborazione consiste in “decodifica, ridimensionamento, ricodifica” nei formati JPEG, PNG, WebP, AVIF o HEIC.
- La CI continua a inciampare nelle ricompilazioni dei binari precompilati di Sharp, oppure distribuisci container Alpine e hai già incontrato il limite della dipendenza di sistema libvips.
La valutazione onesta a maggio 2026 è questa: Bun.Image copre il “95% dei casi comuni di Sharp”, con un ingombro di installazione molto più ridotto e senza la trafila degli addon nativi. Il restante 5% è proprio dove la maturità di libvips continua a rendere prezioso Sharp. Conviene quindi prevedere una fase dual stack, senza rimuoverlo ovunque dal primo giorno: migra le route adatte ai punti di forza di Bun.Image, conserva Sharp nelle altre e rivaluta la separazione con Bun 1.4.x, quando probabilmente arriveranno la rotazione libera e i frame animati.
Le tre funzionalità da tenere d’occhio nei prossimi aggiornamenti sono rotazione libera, accesso ai frame dei WebP animati e mantenimento dei profili ICC: considerata la velocità di sviluppo legata alla riscrittura in Rust, sono le candidate più probabili per le prossime release. Segui il changelog di Bun e riesamina la divisione dual stack a ogni minor release.
Il commit di migrazione che porterei davvero in produzione
Ecco una route di produzione dopo la sostituzione, completa di gestione degli errori e vincolo sulla versione del runtime:
// 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;Ci sono tre elementi da fissare in modo esplicito.
La riga "engines": { "bun": ">=1.3.14" } nel package.json è essenziale. Bun.Image è stato introdotto nella 1.3.14: con le versioni precedenti, a runtime compare Bun.image is not a function. È meglio che il problema emerga come errore di installazione anziché come un 500 in produzione.
Il pacchetto bun-types, che sostituisce @types/bun, include i tipi di Bun.Image a partire dalla 1.3.14. tsc --noEmit viene completato senza shim @ts-expect-error. Se l’editor continua a sottolineare in rosso Bun.image, la versione fissata di bun-types è troppo vecchia.
Piano di rollback: mantieni sharp in optionalDependencies per un ciclo di rilascio, usando come fallback l’approccio dual stack con import dinamico della sezione sui limiti. Dopo una settimana di metriche di produzione positive, rimuovi sharp da optionalDependencies ed elimina il ramo di fallback. Non fare entrambe le cose nello stesso commit. Se preferisci la prudenza, non farle neppure nella stessa settimana.
Il vero test per stabilire se una funzionalità di Bun è pronta per la produzione non è ciò che dichiara il changelog, ma la disponibilità a inserirla in un proprio commit. Questa, per me, può andare in produzione.
Bun.Image funziona anche fuori da Bun, per esempio in Node.js?
No. Bun.Image è integrato nel runtime, non è un pacchetto npm. Se serve un’alternativa a Sharp utilizzabile sia con Node sia con Bun, si possono valutare pacchetti di terze parti come bun-image-turbo; altrimenti, conviene restare su Sharp.
L’API è davvero un sostituto diretto di Sharp o ne riprende soltanto l’idea?
La struttura della catena e i nomi dei metodi sono volutamente compatibili con Sharp: costruttore → .resize / .rotate / .flip / .modulate → terminale .webp / .jpeg / .png / .avif. Nella maggior parte dei punti basta sostituire la riga di import. Le quattro differenze sono rotazione libera, frame animati, mantenimento dei profili ICC e .tile().
Quali componenti usa Bun.Image internamente?
libjpeg-turbo per decodificare e codificare JPEG, spng per PNG, libwebp per WebP e AVIF, oltre ai kernel geometrici SIMD di Bun per il ridimensionamento fixed-point i16. Tutto è compilato nel binario di Bun: nessun addon nativo e nessuna ricompilazione.
Come si confrontano le prestazioni di Bun.Image e Sharp nel ridimensionamento?
Nei casi comuni di ridimensionamento e ricodifica JPEG/PNG, su hardware locale la differenza resta entro ~8%. libvips di Sharp continua a essere più veloce nello streaming di immagini molto grandi e nei carichi animati. Il vantaggio principale di Bun.Image riguarda tempi di installazione e memoria, non la CPU impiegata per ridimensionare una singola immagine.
Conviene migrare adesso?
Sì, se il runtime è Bun e la pipeline decodifica, ridimensiona e ricodifica JPEG/PNG/WebP/AVIF. Se dipendi dai frame dei WebP animati, dal mantenimento dei profili colore ICC o dal deep zoom .tile(), conserva Sharp in quei percorsi e adotta un approccio dual stack.
5 set 2026







