Bun Image: como migrar do Sharp em 4 etapas

Veja como migrar do Sharp para Bun Image em 4 etapas, reduzir o tempo de CI e manter o Sharp apenas onde animações, ICC e deepzoom ainda exigem.

Saturday, September 5, 2026Omid Saffari
Bun Image: como migrar do Sharp em 4 etapas

O Bun Image chegou no Bun v1.3.14 em 13 de maio de 2026. Exposto pela API Bun.Image, ele combina libjpeg-turbo + spng + libwebp em um pipeline que espelha a API do Sharp e dispensa qualquer etapa de build de addon nativo. Depois de três anos vendo o CI quebrar no binário libvips do lovell/sharp toda vez que eu atualizava o Node, foi essa versão que me fez remover o Sharp.

Por que abandonei o Sharp assim que o Bun Image 1.3.14 chegou

As notas de lançamento do Bun 1.3.14 saíram em 13 de maio com a lista habitual de novidades. Escondida entre “cliente HTTP/3” e “instalações com cache 7x mais rápidas” estava a linha que encerrou meu ciclo com o Sharp: Bun.Image, um pipeline de imagens encadeável, integrado ao runtime, com libjpeg-turbo, spng e libwebp compilados diretamente no binário do Bun.

Se você nunca perdeu um dia de CI por causa do Sharp, pode pular este parágrafo. Se já perdeu, conhece o roteiro. sharp/lib/sharp-linuxmusl-x64.node não encontrado. Cannot find module '../build/Release/sharp.node' no container Alpine. O cache de camadas do Docker é invalidado porque alguém atualizou o Node e, de repente, npm rebuild sharp roda do zero a cada push. O build da Vercel tenta acessar a CDN dos binários pré-compilados e estoura o tempo limite. Foram três anos assim, em três projetos diferentes. Em algum momento desse período, comecei a digitar apk add --no-cache vips-dev de memória.

Bun.Image é a resposta nativa do runtime. O processamento de imagens não exige npm install: os codecs já vêm no binário do Bun. Também não há addon nativo para recompilar quando a ABI do Node muda, porque não há Node nem addon. Os kernels geométricos usam SIMD de ponto fixo i16, e a decodificação de JPEG reduz automaticamente a imagem ao menor tamanho suficiente. Em termos de arquitetura, é o que o Sharp poderia ter sido se não precisasse existir como addon do Node.

O Bun 1.3.14 também importou por outro motivo: esta é a última versão em Zig antes da chegada da reescrita em Rust financiada pela Anthropic. O The Register cobriu o ritmo de merges em 14 de maio, e a velocidade de engenharia daqui em diante deve ser forte. Prefiro migrar agora para uma primitiva do Bun a carregar uma dependência de addon nativo durante a reescrita do runtime.

Isto não é um obituário do Sharp. Com libvips, ele continua sendo o campeão de velocidade para WebP animado, trabalhos fotográficos em que o perfil de cor é crítico e a pirâmide deepzoom de tile(). O que vem a seguir é o roteiro para os 95% do processamento de imagens que não envolvem esses três casos: o pipeline de decodificar, redimensionar e codificar usado pela maioria das aplicações em produção.

Migração para Bun Image em 4 etapas no meu pipeline de capas

Fiz a troca no omidsaffari-admin, o worker que trata a saída de capas do gpt-image-2 antes de enviá-la ao R2. Eram oito pontos de uso do Sharp em um único arquivo, todos no caminho executado depois da etapa cover do PublishWorkflow. A migração inteira levou 42 minutos, incluindo o fluxo de codificação dupla em WebP com fallback para JPEG.

Etapa 1 – Mapeie onde o Sharp aparece. Antes de mudar qualquer coisa, encontre todos os imports:

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

O objetivo é saber desde o início se a troca envolve quatro pontos de uso ou quarenta. Se forem quarenta, migre rota por rota, não tudo de uma vez.

Etapa 2 – Troque o import por Bun.file().image(). O construtor do Sharp aceita um caminho, um Buffer ou uma Stream. Já o construtor do Bun.Image recebe um caminho via Bun.file(), um Uint8Array, um Blob ou qualquer valor retornado pelas primitivas de arquivo do Bun — inclusive referências de Bun.s3(), o que mudou a forma do meu código.

Etapa 3 – Converta o encadeamento. É aqui que o Bun.Image justifica a descrição de “compatível com Sharp”. Todos os métodos que eu usava em produção tiveram correspondência 1:1: .resize(w, h, { fit: "cover" }) é idêntico; .rotate(90) funciona, com a ressalva sobre rotação explicada abaixo; .flip() e .flop() são iguais; .modulate({ brightness, saturation }) também. Os métodos terminais de formato — .webp({ quality }), .jpeg({ quality }), .png(), .avif() e .heic() — estão todos disponíveis.

Etapa 4 – Substitua o método terminal. O .toBuffer() do Sharp vira .toBuffer() no Bun.Image. O retorno é Uint8Array, não Buffer — detalhe importante se o destino verificar especificamente o tipo Buffer. O .toFile(path) do Sharp vira .write(path). O contrato de execução preguiçosa é o mesmo: nada roda até que o método terminal seja aguardado com await.

Este é o diff real de um dos handlers de rota:

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

Para o caso mais comum, a troca inteira está aí: uma linha de import e uma chamada ao construtor.

Depois da migração, bun pm ls | grep sharp não retorna nada. O Dockerfile do CI perde a linha RUN apk add --no-cache vips-dev. A imagem final fica ~80MB menor. O package.json passa a ter uma dependência e um aviso de peer dependency a menos.

Três pontos que ainda não têm equivalência perfeita

Vale encarar estas limitações antes de começar a arrancar o Sharp do projeto. Se o seu pipeline faz mais do que apenas redimensionar e codificar, pelo menos uma delas pode aparecer.

Armadilha 1 – Preservação do perfil de cor ICC. O .withMetadata({ icc: "p3" }) do Sharp mantém o perfil de cor da entrada durante a codificação. No Bun.Image 1.3.14, o ICC é removido. Em fluxos sRGB de entrada e saída — a maior parte das imagens para web — isso não é perceptível. Em pipelines fotográficos nos quais alguém envia uma imagem Display-P3 de ampla gama e espera que o perfil seja preservado, o Sharp ainda leva vantagem. Se o uso do Bun.Image for obrigatório, a saída é ler o bloco ICC com exifr, codificar e depois recolocá-lo manualmente. Não é uma solução elegante.

Armadilha 2 – Frames de WebP animado e GIF. O Bun.Image decodifica apenas o primeiro frame de uma entrada animada e descarta os demais. Não há equivalente para { animated: true } do Sharp com acesso frame a frame. Se o seu trabalho envolve processar sprite sheets, gerar miniaturas animadas ou percorrer frames por qualquer motivo, esse é um limite incontornável. Mantenha o Sharp nesses caminhos do código.

Armadilha 3 – A pirâmide de .tile(). O Sharp herda do libvips a geração de tiles deepzoom / IIIF. Para quem mantém um servidor de imagens no estilo Leaflet, um pipeline de map tiles ou uma interface de zoom para acervo de museu, isso não é opcional. O Bun.Image não tem uma primitiva de tiles e provavelmente continuará assim por algum tempo: o libvips reúne décadas de trabalho, e a equipe do Bun deve priorizar primeiro os casos mais comuns.

Há ainda uma ressalva menor: o .rotate(45) do Sharp aceita rotação em qualquer ângulo com interpolação bilinear. O .rotate() do Bun.Image aceita apenas 90, 180 e 270. Para 99% das capas e miniaturas de produtos, isso não faz diferença. Já para correção de inclinação ou efeitos visuais angulados, é um impedimento.

Hoje, nos workloads que esbarram em qualquer uma dessas limitações, uso uma estratégia dual stack: Bun.Image no caminho comum e Sharp fixado em uma worker thread apenas nos 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();
}

O import dinâmico mantém o Sharp fora do bundle nos destinos de deploy que nunca passam pelo caminho lento.

Os números de CI e cold start

A diferença no tempo de instalação é o resultado que mais me interessa, porque minutos de CI se acumulam.

No meu runner de CI Ubuntu x86_64, o bun install com o Sharp fixado levava 4.8s com o cache aquecido. Depois de remover o Sharp do package.json, caiu para 1.4s. O ganho vem de eliminar o download do binário pré-compilado do Sharp e a verificação da dependência opcional de sistema do libvips.

Uma instalação fria, sem ~/.bun/install/cache e sem node_modules, caiu de 18.2s para 7.1s. Remover um único addon nativo de uma árvore com 200 pacotes normalmente não altera tanto o tempo de instalação. A diferença fora da curva acontece porque o postinstall do Sharp era a etapa individual mais lenta da árvore.

O Bun 1.3.14 também traz o armazenamento global do linker isolado, descrito nas notas de lançamento como “instalações com cache 7x mais rápidas” no projeto inteiro. Somando isso à remoção do Sharp, o ciclo completo de bun install com cache aquecido no repositório administrativo caiu de 6.4s para 1.1s. É o tipo de mudança perceptível no desenvolvimento local: bun add some-package deixa de ser uma pausa para o café.

As imagens Docker encolheram cerca de 80MB após a retirada do binário pré-compilado do Sharp e do pacote Alpine vips-dev exigido pelo fallback do prebuild. O CI também acerta mais vezes o cache de camadas, porque a camada anterior à instalação permanece estável diante de mais mudanças de dependências. O download do binário pré-compilado do Sharp era um dos gatilhos mais ruidosos de invalidação desse cache.

Procuro manter minha cadeia de desenvolvimento enxuta — foi o mesmo impulso que me levou a publicar os hooks do Claude Code 2.1.141 no dia em que chegaram — porque o efeito acumulado de pequenos ganhos nas ferramentas é o que torna uma operação individual competitiva. Um bun install 5 segundos mais rápido parece pouco. Agora multiplique por 80 commits por semana.

Minha maior preocupação era a latência por imagem. Em um redimensionamento representativo — PNG de 1024×1024 para WebP de 512×512 com qualidade 82 — o Sharp 0.34.2 e o Bun.Image ficaram a uma distância de até 8% no meu M2 local. Para um workload web, nenhuma das diferenças é relevante.

Há uma explicação arquitetural para o Bun.Image competir no redimensionamento bruto, mesmo com apenas dezoito meses de existência contra as duas décadas do libvips: kernels SIMD de redimensionamento em ponto fixo i16 e escalonamento IDCT de JPEG para o menor tamanho suficiente já na decodificação. Em vez de decodificar um JPEG de 4000×4000 em um bitmap completo para só então reduzi-lo, o Bun.Image faz a decodificação diretamente na resolução de destino. O Sharp aplica o mesmo truque via libjpeg-turbo, por isso os dois pipelines chegam a resultados tão próximos.

É na memória que o Bun.Image abre uma vantagem mais visível: o empréstimo zero-copy de ArrayBuffer mantém o pico de RSS abaixo do Sharp em lotes de 50+ imagens. Isso importa ao processar galerias em uma única execução de worker. Quando há apenas uma imagem por requisição, o ganho desaparece.

Quando o Sharp ainda vence — e quando migrar para Bun Image

Continue com o Sharp se algum destes pontos se aplicar:

  • Você precisa acessar cada frame de um WebP animado.
  • Seu fluxo preserva perfis de cor ICC em fotografias de ampla gama.
  • Você depende da pirâmide deepzoom de .tile() em workloads de servidor de imagens ou map tiles.
  • Você precisa girar em qualquer ângulo com interpolação.
  • Seus destinos de deploy rodam apenas Node — funções Node da Vercel, runtime Node do AWS Lambda ou Cloudflare Workers, onde o Bun ainda não está disponível.

Migre para Bun.Image se todos estes pontos se aplicarem:

  • Você já usa o runtime Bun em pelo menos uma camada.
  • Seu processamento de imagens se resume a “decodificar, redimensionar e recodificar” em JPEG, PNG, WebP, AVIF ou HEIC.
  • Seu CI sofre com rebuilds dos binários pré-compilados do Sharp, ou você publica containers Alpine e já esbarrou na dependência de sistema do libvips.

O diagnóstico honesto em maio de 2026 é este: Bun.Image entrega “95% do Sharp nos casos comuns”, com uma instalação muito menor e nenhuma cerimônia de addon nativo. Os 5% restantes estão justamente onde a maturidade do libvips ainda faz o Sharp valer a pena. Por isso, planeje um período de dual stack em vez de arrancar o Sharp de tudo no primeiro dia. Migre as rotas que se encaixam no ponto forte do Bun.Image, mantenha o Sharp nas demais e reavalie a divisão quando o Bun 1.4.x provavelmente trouxer rotação livre e frames animados.

Na lista de recursos a acompanhar estão rotação livre, acesso a frames de WebP animado e preservação de ICC. Dada a velocidade de engenharia da reescrita em Rust, esses são os três candidatos mais prováveis a chegar em seguida. Assine o changelog do Bun e revise a divisão da sua dual stack a cada versão minor.

O commit exato de migração que eu publicaria

Esta é uma rota de produção depois da troca, já com tratamento de erros e a versão mínima em 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;

Há três detalhes que merecem ser fixados de forma explícita.

A linha "engines": { "bun": ">=1.3.14" } no package.json é essencial. Bun.Image entrou na versão 1.3.14; versões anteriores falham em runtime com Bun.image is not a function. É melhor descobrir isso como erro de instalação do que como um 500 em produção.

O pacote bun-types, substituto de @types/bun, inclui os tipos do Bun.Image a partir da versão 1.3.14. O tsc --noEmit passa sem remendos de @ts-expect-error. Se o editor ainda marca Bun.image em vermelho, a versão fixada de bun-types está antiga demais.

Plano de rollback: mantenha sharp em optionalDependencies durante um ciclo de release e use como fallback a dual stack com import dinâmico da seção de limitações. Depois de uma semana de métricas verdes em produção, remova sharp de optionalDependencies e apague o branch de fallback. Não faça as duas mudanças no mesmo commit. Se você for cauteloso, não faça nem na mesma semana.

O teste que define se um recurso do Bun está pronto para produção não é o que diz o changelog. É saber se você o colocaria no próprio commit. Este aqui vai para produção.

Bun.Image funciona fora do Bun, no Node.js?

Não. Bun.Image faz parte do runtime; não é um pacote npm. Se você precisa de uma alternativa ao Sharp que funcione tanto em Node quanto em Bun, considere pacotes de terceiros como bun-image-turbo ou continue com o Sharp.

A API substitui mesmo o Sharp diretamente ou só foi inspirada nele?

O formato do encadeamento e os nomes dos métodos foram pensados para serem compatíveis com Sharp: construtor → .resize / .rotate / .flip / .modulate → terminal .webp / .jpeg / .png / .avif. Na maioria dos pontos de uso, basta trocar a linha de import. As quatro diferenças são rotação livre, frames animados, preservação de ICC e .tile().

O que o Bun.Image usa por baixo dos panos?

libjpeg-turbo para decodificar e codificar JPEG, spng para PNG, libwebp para WebP e AVIF, além dos kernels geométricos SIMD do próprio Bun, com redimensionamento em ponto fixo i16. Tudo é compilado no binário do Bun: não há addon nativo nem etapa de rebuild.

Como o desempenho do Bun.Image se compara ao Sharp no redimensionamento?

Nos casos comuns de redimensionar e recodificar JPEG/PNG, a diferença entre os dois fica em até ~8% no hardware local. O libvips do Sharp ainda é mais rápido para streaming de imagens muito grandes e workloads animados. O principal ganho do Bun.Image está no tempo de instalação e na memória, não na CPU bruta de um único redimensionamento.

Devo migrar agora?

Sim, se você usa o runtime Bun e seu pipeline decodifica, redimensiona e recodifica JPEG/PNG/WebP/AVIF. Se depende de frames de WebP animado, preservação de perfil ICC ou deepzoom com .tile(), mantenha o Sharp nesses caminhos e opere em dual stack.

Última atualização

5 de set. de 2026

CategoriaBuild

Prefira este site no Google

Adicionar omidsaffari.com como fonte preferida na Busca do Google

Marque omidsaffari.com como fonte preferida e o Google destaca o site para você em Top Stories, AI Overviews e AI Mode.

Newsletter

Uma carta, todo domingo. Sistemas que funcionam, não hot takes.

Build logs, sistemas em produção e notas de campo de um portfólio de ventures de IA.

Semanal. Sem spam. Cancele quando quiser.