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
Categoria
Build

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.

Artigos relacionados
Cursor preço: o Rollouts é grátis? Planos, créditos e custos

Cursor preço: o Rollouts é grátis? Planos, créditos e custos

Cursor Rollouts exige Teams ou Enterprise. Entenda os créditos de lançamento por 10 dias, cerca de 50 ou 500 alterações, e os custos a conferir.24 de set. de 2026Build
Unreal Agent na prática: como avaliar um agente de IA

Unreal Agent na prática: como avaliar um agente de IA

Veja como testar o Unreal Agent, um agente de IA para repositórios: isole o ambiente, salve a sessão em JSONL e meça custo, segurança e desempenho.24 de set. de 2026Build
Codex JetBrains com Air: do primeiro prompt à revisão

Codex JetBrains com Air: do primeiro prompt à revisão

Aprenda a instalar o Air Alpha, conectar o Codex ao JetBrains, fornecer o contexto certo e revisar a primeira alteração de código com segurança.23 de set. de 2026Build
JetBrains Air é grátis? Entenda quem paga cada camada

JetBrains Air é grátis? Entenda quem paga cada camada

JetBrains Air é grátis no plugin, mas agente, IDE e uso de API podem gerar custos. Veja as quatro formas de autorização e descubra qual conta paga.23 de set. de 2026Build
Firecrawl API com hospedagem própria: instalação e custos

Firecrawl API com hospedagem própria: instalação e custos

Entenda como instalar a Firecrawl API em infraestrutura própria, validar scrapes reais e comparar o custo operacional com o Firecrawl Cloud em 30 dias.22 de set. de 2026Build
Agentes de IA: quando um retry pago exige aprovação humana

Agentes de IA: quando um retry pago exige aprovação humana

Entenda por que retries pagos de agentes de IA precisam de aprovação humana no ponto da recompra, mesmo em fluxos que já mantêm pessoas no circuito.22 de set. de 2026Build
Automação com IA no MindStudio: preços, limites e quando vale a pena

Automação com IA no MindStudio: preços, limites e quando vale a pena

Veja como o MindStudio organiza automação com IA, quanto custam os planos e o uso de modelos e quais limites testar antes de adotar a plataforma.22 de set. de 2026Build
Wispr Flow ou Superwhisper: qual app de ditado compensa?

Wispr Flow ou Superwhisper: qual app de ditado compensa?

Compare Wispr Flow e Superwhisper em preço, privacidade, uso offline e recursos para equipes antes de escolher seu aplicativo de ditado por voz.22 de set. de 2026Build
Newsletter

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

Semanal. Sem spam. Cancele quando quiser.