Bun Image en Bun 1.3.14: migración desde Sharp en 4 pasos

Migra de Sharp a Bun Image en 4 pasos: equivalencias de API, límites con ICC y WebP animado, cifras de CI y una estrategia segura de despliegue.

Saturday, September 5, 2026Omid Saffari
Bun Image en Bun 1.3.14: migración desde Sharp en 4 pasos

Bun lanzó la versión v1.3.14 el 13 de mayo de 2026 con Bun Image, un canal basado en libjpeg-turbo + spng + libwebp que replica la API de Sharp y funciona sin un solo paso de compilación de addons nativos. Después de tres años viendo cómo el binario libvips de lovell/sharp rompía la CI cada vez que actualizaba Node, esta fue la versión que me convenció de sacar Sharp.

Por qué abandoné Sharp en cuanto llegó Bun Image 1.3.14

Las notas de la versión Bun 1.3.14 se publicaron el 13 de mayo con la lista habitual de novedades. Entre «cliente HTTP/3» e «instalaciones en caliente 7x más rápidas» aparecía la línea que puso fin a mi etapa con Sharp: Bun.Image, un canal de procesamiento de imágenes encadenable e integrado en el entorno de ejecución, con libjpeg-turbo, spng y libwebp compilados directamente en el binario de Bun.

Si nunca perdió un día de CI por culpa de Sharp, puede saltarse este párrafo. Quien sí lo haya sufrido conoce el guion: no se encuentra sharp/lib/sharp-linuxmusl-x64.node; aparece Cannot find module '../build/Release/sharp.node' en el contenedor Alpine; la caché de capas de Docker queda invalidada porque alguien actualizó Node y ahora npm rebuild sharp se ejecuta desde cero con cada push; o la compilación de Vercel intenta acceder al CDN de binarios precompilados y agota el tiempo de espera. Tres años así, en tres proyectos distintos, bastaron para que terminara escribiendo apk add --no-cache vips-dev de memoria.

Bun.Image es la respuesta nativa del entorno de ejecución. El procesamiento de imágenes no requiere ningún paso npm install: los códecs vienen dentro del binario de Bun. Tampoco hay un addon nativo que recompilar cuando cambia la ABI de Node, porque no intervienen ni Node ni un addon. Los kernels geométricos usan SIMD de punto fijo i16, y la decodificación JPEG reduce automáticamente la imagen al menor tamaño suficiente. Por su arquitectura, es lo que Sharp habría sido si no tuviera que funcionar como addon de Node.

La otra razón por la que 1.3.14 fue importante para mí es que se trata de la última versión en Zig antes de que llegue la reescritura en Rust financiada por Anthropic. The Register informó el 14 de mayo sobre el ritmo de integración, y a partir de aquí la velocidad de ingeniería será considerable. Prefiero migrar ahora a una primitiva de Bun que arrastrar una dependencia de addon nativo durante la reescritura del entorno de ejecución.

Esto no es un obituario de Sharp. Sharp sobre libvips sigue siendo la opción más rápida para WebP animado, trabajos fotográficos donde el perfil de color es crítico y la pirámide deep zoom de tile(). Lo que sigue es una guía para el 95% del trabajo con imágenes que no pertenece a esos tres casos: el canal de decodificar, redimensionar y codificar que ejecuta la mayoría de las aplicaciones en producción.

Cómo migrar de Sharp a Bun Image en 4 pasos

Hice este cambio en omidsaffari-admin, el worker que posprocesa las portadas generadas por gpt-image-2 antes de enviarlas a R2. Había ocho puntos de uso de Sharp en un solo archivo, todos en la ruta que se ejecuta después del paso cover de PublishWorkflow. La migración completa tomó 42 minutos, incluida la ruta de doble codificación WebP con JPEG como alternativa.

Paso 1: audite dónde usa Sharp. Antes de cambiar nada, localice todas las importaciones:

Bash
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/

Conviene saber desde el principio si hay que sustituir cuatro puntos de uso o cuarenta. Si son cuarenta, migre ruta por ruta, no todo de una vez.

Paso 2: sustituya la importación por Bun.file().image(). El constructor de Sharp acepta una ruta, un Buffer o un Stream. El constructor de Bun.Image acepta una ruta mediante Bun.file(), un Uint8Array, un Blob o cualquier valor devuelto por las primitivas de archivos de Bun, incluidas las referencias de Bun.s3(), algo que cambió la estructura de mi código.

Paso 3: traduzca la cadena de operaciones. Aquí es donde Bun.Image se gana la etiqueta de «compatible con Sharp». Todos los métodos que tenía en producción encontraron una equivalencia 1:1: .resize(w, h, { fit: "cover" }) es idéntico; .rotate(90) funciona, con la salvedad sobre rotación que explico más adelante; .flip() y .flop() se mantienen; y .modulate({ brightness, saturation }) también. Están disponibles todos los métodos terminales de formato: .webp({ quality }), .jpeg({ quality }), .png(), .avif() y .heic().

Paso 4: cambie el método terminal. El .toBuffer() de Sharp pasa a ser el .toBuffer() de Bun.Image. Este devuelve Uint8Array, no Buffer, un detalle importante si el siguiente componente comprueba expresamente el tipo Buffer. El .toFile(path) de Sharp se convierte en .write(path). El contrato de ejecución diferida es idéntico: nada se procesa hasta que se espera el resultado del método terminal.

Este es el diff real de uno de mis manejadores de ruta:

TypeScript
// 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;
}

Eso es todo lo que hay que sustituir en el caso habitual: una línea de importación y una llamada al constructor.

Después de la migración, bun pm ls | grep sharp no devuelve nada. El Dockerfile de CI pierde la línea RUN apk add --no-cache vips-dev. La imagen resultante ocupa ~80MB menos. Y package.json se queda con una dependencia y una advertencia de peer dependency menos.

Tres diferencias que todavía impiden una migración directa

Conviene revisar estos puntos con franqueza antes de eliminar Sharp, porque al menos uno puede causar problemas si el canal hace algo más que redimensionar y codificar.

Limitación 1: conservación del perfil de color ICC. El método .withMetadata({ icc: "p3" }) de Sharp conserva el perfil de color de entrada durante la codificación. En la versión 1.3.14, Bun.Image elimina el ICC. Esto no se percibe en flujos sRGB de entrada y salida, es decir, en la mayoría del trabajo con imágenes web. Pero si el canal fotográfico recibe una imagen Display-P3 de gama amplia y debe conservarla, Sharp sigue ganando. Si es imprescindible usar Bun.Image, la solución provisional consiste en leer el bloque ICC con exifr, codificar y volver a adjuntarlo manualmente. No es una solución elegante.

Limitación 2: fotogramas de WebP y GIF animados. Bun.Image decodifica el primer fotograma de una imagen animada y descarta los demás. No existe un equivalente de { animated: true } de Sharp con acceso a cada fotograma. Para procesar sprite sheets, generar miniaturas animadas o recorrer fotogramas, este límite es absoluto. Mantenga Sharp en esas rutas de código.

Limitación 3: la pirámide .tile(). Sharp hereda de libvips la generación de mosaicos deep zoom / IIIF. Si mantiene un servidor de imágenes al estilo Leaflet, un canal de mosaicos de mapas o una interfaz de zoom de nivel museístico, esta función es indispensable. Bun.Image no tiene una primitiva de mosaicos y probablemente tardará en incorporarla: libvips acumula décadas de trabajo, y el equipo de Bun priorizará primero el caso común.

Una limitación menor que también merece atención: .rotate(45) de Sharp admite rotaciones de cualquier ángulo con interpolación bilineal. .rotate() de Bun.Image solo acepta 90, 180 y 270. Para el 99% del trabajo con portadas y miniaturas de productos no supone ningún problema. Si el proceso corrige inclinaciones o aplica giros con fines estéticos, sí es un bloqueo.

Para las cargas que presentan alguna de estas condiciones, ahora uso un patrón de doble pila: Bun.Image para la ruta común y Sharp fijado en un hilo de worker solo para los casos problemáticos:

TypeScript
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();
}

La importación dinámica mantiene a Sharp fuera del bundle en los destinos de despliegue que nunca pasan por la ruta lenta.

Cifras de CI y arranque en frío con Bun Image

La diferencia en el tiempo de instalación es el dato que más me importa, porque los minutos de CI se acumulan.

En mi runner de CI Ubuntu x86_64, bun install tardaba 4.8s en caliente con Sharp fijado. Después de quitar Sharp de package.json, bajó a 1.4s en caliente. El ahorro viene de omitir la descarga del binario precompilado de Sharp y la comprobación de la dependencia opcional del sistema libvips.

La instalación en frío, sin ~/.bun/install/cache ni node_modules, pasó de 18.2s a 7.1s. Quitar un solo addon nativo de un árbol de 200 paquetes no suele reducir tanto el tiempo de instalación; la caída desproporcionada se debe a que el postinstall de Sharp era el paso individual más lento del árbol.

Bun 1.3.14 también incorpora el almacén global del enlazador aislado, que las notas de la versión describen como «instalaciones en caliente 7x más rápidas» en el proyecto completo. Al combinarlo con la eliminación de Sharp, el ciclo completo de bun install en caliente de mi repositorio de administración pasó de 6.4s a 1.1s. Es el tipo de cambio que se nota durante el desarrollo local: bun add some-package deja de ser una pausa para ir por café.

Las imágenes de Docker redujeron su tamaño en unos 80MB al eliminar tanto el binario precompilado de Sharp como el paquete vips-dev de Alpine que requería la alternativa cuando fallaba el binario. También aumentaron los aciertos de la caché de capas en CI, porque la capa situada debajo del paso de instalación se mantiene estable ante más cambios de dependencias; la descarga del binario precompilado de Sharp era uno de los factores más ruidosos de invalidación.

En general procuro mantener ajustadas mis herramientas de desarrollo. Ese mismo criterio me llevó a publicar los hooks de Claude Code 2.1.141 el mismo día de su lanzamiento, y el efecto acumulado de estas pequeñas mejoras es lo que permite competir a un negocio de una sola persona. Ahorrar 5 segundos en bun install parece poco. Multiplíquelo por 80 commits a la semana.

La latencia por imagen era el dato que más me preocupaba. En una conversión representativa —un PNG de 1024×1024 a un WebP de 512×512 con calidad 82—, Sharp 0.34.2 y Bun.Image quedaron a una distancia inferior al 8% en mi M2 local. Ninguna de las dos diferencias resulta relevante para una carga web.

Hay una razón arquitectónica para que Bun.Image compita en redimensionamiento puro a pesar de tener dieciocho meses, frente a las dos décadas de libvips: sus kernels SIMD de redimensionamiento con punto fijo i16, sumados al escalado IDCT de JPEG al menor tamaño suficiente durante la decodificación. No decodifica un JPEG de 4000×4000 en un bitmap completo para después reducirlo; Bun.Image lo decodifica directamente a la resolución objetivo. Sharp aplica el mismo recurso mediante libjpeg-turbo, por eso ambos canales terminan en un rango parecido.

En memoria, Bun.Image sí toma una ventaja visible: el préstamo sin copias de ArrayBuffer mantiene el RSS máximo por debajo de Sharp en lotes de 50+ imágenes. Esto importa al procesar galerías en una sola invocación del worker. Si se procesa una imagen por solicitud, la mejora pasa inadvertida.

Cuándo sigue ganando Sharp y cuándo conviene migrar

Mantenga Sharp si se cumple alguna de estas condiciones:

  • Necesita acceder fotograma por fotograma a un WebP animado.
  • Debe conservar perfiles de color ICC en trabajos fotográficos de gama amplia.
  • Depende de la pirámide deep zoom de .tile() para un servidor de imágenes o cargas de mosaicos de mapas.
  • Necesita rotaciones de cualquier ángulo con interpolación.
  • Despliega solo en destinos para Node: funciones Node de Vercel, el entorno de ejecución Node de AWS Lambda o Cloudflare Workers, donde Bun todavía no es una opción.

Migre a Bun.Image si se cumplen todas estas condiciones:

  • Ya utiliza el entorno de ejecución Bun en al menos una capa.
  • Su trabajo con imágenes consiste en «decodificar, redimensionar y volver a codificar» en JPEG, PNG, WebP, AVIF o HEIC.
  • Su CI sufre con las recompilaciones de binarios precompilados de Sharp, o despliega contenedores Alpine y ya chocó con la dependencia de sistema libvips.

La evaluación honesta en mayo de 2026 es que Bun.Image cubre «el 95% del caso común de Sharp», con una instalación mucho más pequeña y sin la ceremonia de los addons nativos. El 5% restante es justo donde la madurez de libvips en Sharp sigue justificando su presencia. Por eso conviene planificar una etapa de doble pila: no elimine Sharp de todas partes el primer día. Migre las rutas que encajan en el punto fuerte de Bun.Image, conserve Sharp en las demás y vuelva a evaluar la división cuando llegue Bun 1.4.x, momento en el que probablemente estén disponibles la rotación arbitraria y los fotogramas animados.

Lista de funciones que sigo para futuras actualizaciones: rotación arbitraria, acceso a fotogramas de WebP animado y conservación de ICC. Son las tres que tienen más probabilidades de llegar después, dado el ritmo de ingeniería de la reescritura en Rust. Suscríbase al changelog de Bun y vuelva a auditar la división de la doble pila en cada versión menor.

El commit exacto que usaría para la migración

Así queda una de mis rutas de producción después del cambio, incluido el manejo de errores y la versión mínima en engines:

TypeScript
// 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;

Hay tres elementos que conviene fijar de forma explícita:

La línea "engines": { "bun": ">=1.3.14" } de package.json es imprescindible. Bun.Image se incorporó en 1.3.14; las versiones anteriores fallan durante la ejecución con Bun.image is not a function, y es preferible que el problema aparezca como un error de instalación, no como un 500 en producción.

El paquete bun-types, que sustituye a @types/bun, incluye los tipos de Bun.Image a partir de 1.3.14. tsc --noEmit funciona sin soluciones provisionales con @ts-expect-error. Si el editor todavía subraya Bun.image en rojo, la versión fijada de bun-types es demasiado antigua.

Plan de reversión: mantenga sharp en optionalDependencies durante un ciclo de lanzamiento, con la doble pila y la importación dinámica de la sección de limitaciones como ruta alternativa. Después de una semana con métricas de producción en verde, quite sharp de optionalDependencies y elimine la rama alternativa. No haga ambas cosas en el mismo commit. Si prefiere ser muy cauteloso, tampoco las haga en la misma semana.

La prueba de que una función de Bun está lista para producción no es lo que afirme el changelog, sino si la incluiría en mi propio commit. Esta sí la publico.

¿Bun.Image funciona fuera de Bun, por ejemplo en Node.js?

No. Bun.Image está integrado en el entorno de ejecución; no es un paquete npm. Si necesita una alternativa portátil a Sharp que funcione tanto en Node como en Bun, considere paquetes de terceros como bun-image-turbo o continúe con Sharp.

¿La API sustituye directamente a Sharp o solo se inspira en ella?

La forma de la cadena y los nombres de los métodos buscan ser compatibles con Sharp: constructor → .resize / .rotate / .flip / .modulate → método terminal .webp / .jpeg / .png / .avif. En la mayoría de los puntos de uso basta con cambiar la línea de importación. Las cuatro diferencias son la rotación arbitraria, los fotogramas animados, la conservación de ICC y .tile().

¿Qué utiliza Bun.Image internamente?

libjpeg-turbo para decodificar y codificar JPEG, spng para PNG, libwebp para WebP y AVIF, y los kernels geométricos SIMD propios de Bun, con redimensionamiento de punto fijo i16. Todo viene compilado en el binario de Bun: no hay addon nativo ni paso de recompilación.

¿Cómo se compara el rendimiento de Bun.Image con Sharp al redimensionar?

Al redimensionar y volver a codificar JPEG/PNG en el caso común, ambos quedan a una distancia de ~8% en hardware local. libvips de Sharp sigue siendo más rápido al transmitir imágenes muy grandes y procesar cargas animadas. La mayor ventaja de Bun.Image está en el tiempo de instalación y la memoria, no en la CPU bruta para una sola imagen.

¿Conviene migrar hoy?

Sí, si ya utiliza el entorno de ejecución Bun y su canal decodifica, redimensiona y vuelve a codificar JPEG/PNG/WebP/AVIF. Si depende de fotogramas WebP animados, de conservar perfiles de color ICC o del deep zoom de .tile(), mantenga Sharp en esas rutas y utilice una doble pila.

Última actualización

5 sept 2026

CategoríaBuild

Prefiera este sitio en Google

Añadir omidsaffari.com como fuente preferida en la Búsqueda de Google

Marque omidsaffari.com como fuente preferida y Google lo destacará para usted en Top Stories, AI Overviews y AI Mode.

Newsletter

Una carta, cada domingo. Sistemas que funcionan, no opiniones calientes.

Build logs, sistemas en producción y notas de campo de un portafolio de ventures de IA.

Semanal. Sin spam. Cancele cuando quiera.