TuBrief
구독 채널
비디오
커뮤니티

Substituindo o Sharp no Node.js por recursos nativos do Bun para rodar um servidor de mídia

TuBrief 편집팀
2026년 8월 22일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

Português한국어Español中文العربيةFrançaisBahasa Indonesiaहिन्दी日本語EnglishDeutschРусский

관련 영상

Bun.Image torna todo o seu pipeline de imagens obsoleto4:34

Bun.Image torna todo o seu pipeline de imagens obsoleto

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

Substituindo o Sharp no Node.js por recursos nativos do Bun para rodar um servidor de mídia

Às vezes, uma imagem de Docker que funcionava perfeitamente no ambiente local acaba gerando erros de build no servidor de produção. Se você investigar a causa, o culpado quase sempre é o sharp e a biblioteca libvips, que utilizam bindings nativos em C++. Somando isso às faturas de dezenas de dólares por mês em serviços como Cloudinary ou Imgix, é natural começar a questionar por que todo esse esforço é necessário apenas para adicionar uma funcionalidade básica de processamento de imagens.

Aqui está um guia prático de transição para construir um pipeline de processamento de imagens usando apenas os recursos nativos do runtime único do Bun, sem etapas externas de compilação em C++, reduzindo o tamanho dos containers e economizando custos na nuvem.

Removendo os bindings em C++ que quebram o build no Alpine

Quando você usa o sharp em um ambiente Node.js, ele se conecta dinamicamente às bibliotecas C do sistema operacional através do node-gyp e da camada N-API. Ao tentar criar uma imagem Docker baseada no leve Alpine Linux, um node-gyp rebuild forçado é executado dentro do container devido a incompatibilidades entre as bibliotecas C glibc e musl.

Durante esse processo, toolchains de compilação inteiras como GCC, Python e make entram no container. É muito comum que um código que rodava perfeitamente no ambiente local (macOS ARM64) falhe com um erro de segmentação de memória (SIGSEGV) assim que é enviado para o servidor de produção (Linux x86_64).

O Bun inclui codecs JPEG, PNG e WebP diretamente em seu binário de execução. Não há necessidade de instalar compiladores externos ou pacotes de SO separados, como o libvips.

Item de comparação Node.js (Sharp + libvips) Bun Nativo (Bun.Image)
Dependência de binding C++ node-gyp, N-API obrigatórios Nenhum (incorporado ao binário do runtime)
Toolchain de build Necessário GCC, Python, make Desnecessário
Tamanho do pacote de deploy Centenas de MB incluindo toolchains Reduzido ao nível da imagem base
Erros em tempo de execução Ocorre SIGSEGV em caso de incompatibilidade glibc/musl Evitado por link estático embutido

Substituindo o código do Sharp pelo Bun.Image em proporção 1 para 1

O Bun.Image oferece suporte a uma interface de encadeamento (chaining), permitindo que você migre o código existente do sharp quase sem alterações. Ele manipula Uint8Array diretamente, sem cópias de memória.

`typescript
// Código existente baseado no sharp
import sharp from "sharp";

export async function processImageSharp(inputBuffer: Buffer): Promise {
const image = sharp(inputBuffer);
const metadata = await image.metadata();

if (!metadata.width || metadata.width > 2000) {
return await image
.resize(1024, 1024, { fit: "inside", withoutEnlargement: true })
.rotate(90)
.webp({ quality: 85 })
.toBuffer();
}
return inputBuffer;
}

`

`typescript
// Código convertido para Bun.Image
export async function processImageBun(inputBytes: Uint8Array): Promise {
// Lê apenas o cabeçalho rapidamente para verificar o tamanho sem decodificar todo o bitmap
const meta = await new Bun.Image(inputBytes).metadata();

if (!meta.width || meta.width > 2000) {
return await new Bun.Image(inputBytes)
.resize(1024, 1024, { fit: "inside", withoutEnlargement: true })
.rotate(90)
.webp({ quality: 85 })
.bytes();
}
return inputBytes;
}

`

O método metadata() analisa apenas a área do cabeçalho sem decodificar a imagem inteira. Isso reduz o desperdício de CPU ao lidar com imagens originais grandes.

A correspondência da API por tarefa é a seguinte:

  • Para criar objetos, use new Bun.Image(bytes) ou Bun.file(path).image() em vez de sharp(buf).
  • Para conversão de formato e compressão, mantenha o encadeamento .webp({ quality: 85 }) exatamente igual.
  • Para extrair o binário final, chame .bytes() em vez de .toBuffer() para retornar um Uint8Array.
  • Para gerar imagens de desfoque (blur) para carregamento na UI, utilize o método nativo .placeholder() sem precisar de bibliotecas adicionais.

A ordem de migração é simples: remova sharp e @types/sharp do package.json, altere o formato de saída das funções utilitárias para bytes() e execute a validação de recursos com bun test.

Construindo um cache de miniaturas com o SQLite embutido do Bun

SaaS de imagens como o Cloudinary aumentam os custos drasticamente com qualquer pico de tráfego. No estágio de um serviço desenvolvido por uma única pessoa, você pode criar seu próprio servidor de cache de redimensionamento eficiente combinando apenas bun:sqlite e Bun.serve.

`typescript
import { Database } from "bun:sqlite";

const db = new Database("image_cache.sqlite");
// Aplicando o modo WAL para melhor desempenho de leitura e escrita simultâneas
db.exec("PRAGMA journal_mode = WAL;");
db.exec( CREATE TABLE IF NOT EXISTS image_cache ( key TEXT PRIMARY KEY, data BLOB NOT NULL, placeholder TEXT NOT NULL, mime_type TEXT NOT NULL, created_at INTEGER NOT NULL ));

const selectQuery = db.query("SELECT data, mime_type FROM image_cache WHERE key = ?");
const insertQuery = db.query( INSERT OR REPLACE INTO image_cache (key, data, placeholder, mime_type, created_at) VALUES (?, ?, ?, ?, ?));

export async function getOrGenerateThumbnail(
originalBytes: Uint8Array,
cacheKey: string,
width: number = 300
): Promise<{ bytes: Uint8Array; mimeType: string }> {
const cached = selectQuery.get(cacheKey) as { data: Uint8Array; mime_type: string } | null;
if (cached) {
return { bytes: cached.data, mimeType: cached.mime_type };
}

const imagePipeline = new Bun.Image(originalBytes);
const transformedBytes = await imagePipeline.resize(width).webp({ quality: 80 }).bytes();
const placeholder = await imagePipeline.placeholder();

insertQuery.run(cacheKey, transformedBytes, placeholder, "image/webp", Date.now());

return { bytes: transformedBytes, mimeType: "image/webp" };
}

`

`typescript
// Endpoint de entrega de mídia
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);

if (url.pathname.startsWith("/images/")) {
  const imageId = url.pathname.replace("/images/", "");
  const width = parseInt(url.searchParams.get("w") || "300", 10);
  const cacheKey = `${imageId}_w${width}`;

  const originalFile = Bun.file(`./uploads/${imageId}`);
  if (!(await originalFile.exists())) {
    return new Response("Image Not Found", { status: 404 });
  }

  const originalBytes = await originalFile.bytes();
  const { bytes, mimeType } = await getOrGenerateThumbnail(originalBytes, cacheKey, width);

  return new Response(bytes, {
    headers: {
      "Content-Type": mimeType,
      "Cache-Control": "public, max-age=31536000, immutable",
    },
  });
}

return new Response("Not Found", { status: 404 });

},
});

`

Apenas a primeira requisição recebida passa pelo redimensionamento e é salva no SQLite em formato BLOB; as requisições seguintes são servidas diretamente do cache do banco de dados. Definir um tempo de Cache-Control longo no cabeçalho de resposta também faz com que o cache funcione nos níveis de navegador e CDN.

Evitando o bloqueio do loop principal com Worker Threads

A codificação e decodificação de imagens são tarefas intensivas em CPU. Se você converter imagens no event loop principal quando houver picos de solicitações de upload, a resposta geral do servidor vai congelar. É por isso que a latência P99 dispara na casa das centenas de milissegundos e o servidor de API inteiro acaba travando.

É preciso utilizar a API Worker do Bun para delegar o trabalho de processamento de imagem a uma thread em segundo plano, mantendo o loop principal livre.

`typescript
// imageWorker.ts
declare var self: Worker;

interface ResizeTask {
id: string;
buffer: ArrayBuffer;
width: number;
}

self.onmessage = async (event: MessageEvent) => {
const { id, buffer, width } = event.data;

try {
const inputBytes = new Uint8Array(buffer);
const processedBytes = await new Bun.Image(inputBytes)
.resize(width)
.webp({ quality: 80 })
.bytes();

self.postMessage(
  { id, success: true, buffer: processedBytes.buffer },
  [processedBytes.buffer] as any
);

} catch (error) {
self.postMessage({ id, success: false, error: (error as Error).message });
}
};

`

`typescript
// server.ts
const worker = new Worker("./imageWorker.ts");
const pendingTasks = new Map<string, (buf: ArrayBuffer) => void>();

worker.onmessage = (event) => {
const { id, success, buffer, error } = event.data;
const resolve = pendingTasks.get(id);

if (resolve && success) {
resolve(buffer);
pendingTasks.delete(id);
} else if (!success) {
console.error(Falha na tarefa (${id}):, error);
pendingTasks.delete(id);
}
};

export function dispatchImageJob(id: string, buffer: ArrayBuffer, width: number): Promise {
return new Promise((resolve) => {
pendingTasks.set(id, resolve);
// Transfere apenas a propriedade sem custo de cópia de memória usando um objeto Transferable
worker.postMessage({ id, buffer, width }, [buffer]);
});
}

`

O uso de um Transferable ArrayBuffer elimina o custo de cópia de memória mesmo ao enviar e receber buffers de vários megabytes entre as threads. Mesmo quando há um acúmulo de requisições pesadas, a thread principal responde com 202 Accepted e continua processando outras requisições de API sem atrasos.

Cuidados operacionais importantes antes do deploy

É necessário verificar previamente algumas diferenças que podem surgir entre o ambiente de desenvolvimento local e o container de produção:

  • Verifique o escopo de suporte a codecs: O Bun.Image oferece suporte comum a JPEG, PNG, WebP, GIF e BMP em todos os ambientes Linux, macOS e Windows. Por outro lado, o suporte a HEIC ou AVIF pode variar dependendo do SO do servidor. Em ambientes de produção Linux, é mais seguro fixar o formato de saída padrão em WebP ou JPEG.
  • Limpeza do Dockerfile: Remova completamente instruções de instalação de pacotes de compilação como python3, make, g++ e libvips-dev do Dockerfile. O build se tornará mais rápido e a imagem do container ficará muito mais leve.
  • Permissões do diretório temporário: Se você bloquear o sistema de arquivos do container como Somente Leitura (Read-Only) por motivos de segurança, o diretório /tmp utilizado pelo buffer interno de decodificação do Bun deve obrigatoriamente ter permissão de escrita para evitar falhas no processo.
  • Ativação do modo WAL do SQLite: Se você não executar PRAGMA journal_mode = WAL; logo após a criação do arquivo SQLite, poderá encontrar erros de travamento de banco de dados (DB lock) quando houver um pico de requisições simultâneas de leitura e escrita.

Eliminar dependências externas desnecessárias reduz naturalmente a probabilidade de falhas no build. Apenas aproveitar ativamente as ferramentas embutidas de um runtime único pode aliviar bastante a complexidade operacional e os custos de manutenção da infraestrutura do seu serviço.