Stack bleeding-edge em hardware legado
Voltar para blog

Stack bleeding-edge em hardware legado

07/06/2026 · 5 min · Desenvolvimento

O experimento: Next.js 16 em máquina antiga#

Host de desenvolvimento: ASUS X450LC (mesmo host documentado na auditoria de kernel/NVIDIA) CPU: Intel Core i5-4200U (Haswell, dual-core HT, 1.6 - 2.6 GHz) RAM: 10 GB DDR3 OS: Linux Mint 22.2 (Kernel 6.8 LTS, GPU i915 exclusiva pós-blacklist nouveau) Gerenciador de pacotes original: pnpm Runtime original: Node.js 20 LTS

Stack do projeto:

{
  "dependencies": {
"next": "16.x (Turbopack habilitado)",
"react": "19.0.0-rc",
"tailwindcss": "^4.0",
"@prisma/client": "^5.x",
"auth.js": "^5 (ex-NextAuth)",
"@clerk/nextjs": "^5",
"@dnd-kit/core": "^6",
"@tiptap/react": "^2",
"framer-motion": "^11",
"@react-pdf/renderer": "^3",
"fabric": "^5"
  }
}

A questão de pesquisa: o Bun como substituto drop-in do pnpm + Node.js entrega ganho mensurável de performance de desenvolvimento num dual-core com 10 GB de RAM?

Estratégia de migração segura: zero lock-in#

A migração foi estruturada para ser completamente reversível. O Bun respeita package.json e lê pnpm-lock.yaml como fonte de resolução de dependências, gerando seu próprio bun.lockb sem destruir o lockfile original.

Sequência executada:

# Passo 1: Preservar o lockfile do pnpm (não deletar)
# pnpm-lock.yaml permanece intocado no repositório

# Passo 2: Limpar node_modules (o Bun precisa instalar com sua própria engine)
rm -rf node_modules

# Passo 3: Instalar com Bun - lê package.json e pnpm-lock.yaml
bun install

Saída do bun install (comparativo de tempo):

bun install v1.1.x (...)
 + 847 packages installed [4.23s]   ← Com cache quente do Bun
 + 847 packages installed [12.8s]  ← Instalação limpa (sem cache)

Para referência, o mesmo pnpm install no mesmo hardware levava entre 35 e 55 segundos em instalação limpa. O ganho de 3 a 4x na instalação de dependências é imediato e não requer nenhuma mudança de código.

Plano de rollback se algo quebrasse:

rm -rf node_modules bun.lockb
pnpm install
# De volta ao estado original em ~45 segundos

O primeiro erro: prisma client fantasma após limpeza de node_modules#

Ao tentar subir o servidor de desenvolvimento com injeção de limite de memória (necessário no hardware com 10 GB compartilhados):

NODE_OPTIONS='--max-old-space-size=4096' next dev -H 127.0.0.1

O Turbopack iniciou o compilador, mas abortou com erro fatal:

⨯ ./node_modules/@prisma/client/default.js:2:6
Module not found: Can't resolve '.prisma/client/default'
  1 | module.exports = {
> 2 |   ...require('.prisma/client/default'),
|      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  3 | }

Import trace:
  Edge Middleware:
./node_modules/@prisma/client/default.js
./src/lib/db-client.ts
./src/lib/auth.ts
./middleware.ts

Resolução imediata: regenerar o prisma client#

O rm -rf node_modules apagou o artefato gerado em node_modules/.prisma/client/. O @prisma/client do npm é uma casca vazia: o binário real do Query Engine e os tipos TypeScript gerados precisam ser recriados localmente:

# Limpar cache agressivo do Next.js 16 (Turbopack tem cache próprio em .next/cache)
rm -rf .next

# Regenerar o Prisma Client com o Bun como executor
bun prisma generate

Saída de sucesso:

Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (v5.x.x) to ./node_modules/@prisma/client in 0.9s

O bun prisma generate funciona identicamente ao npx prisma generate ou pnpm prisma generate - o Bun serve de executor de packages binários sem configuração adicional.

O perigo arquitetural revelado pelo import trace#

Além do erro de módulo imediato, o Import trace expôs um problema de arquitetura latente:

Edge Middleware:       ← Contexto de execução: Edge Runtime (V8 Isolated)
  ./src/lib/db-client.ts  ← PrismaClient sendo instanciado aqui
  ./src/lib/auth.ts       ← auth.ts importa db-client.ts
  ./middleware.ts         ← middleware.ts importa auth.ts

O middleware.ts do Next.js executa exclusivamente no Edge Runtime, um ambiente V8 isolado sem APIs Node.js (fs, net, child_process). O Prisma Client padrão depende de um binário Rust compilado que comunica com o banco via TCP, exatamente as APIs ausentes no Edge.

Comportamento em ambiente local vs. produção:

AmbienteComportamento
next dev localPode funcionar silenciosamente (Next.js simula parte do Edge em dev)
Vercel / Cloudflare ProductionFalha garantida em runtime com PrismaClient not supported in Edge

Refatoração aplicada: A lógica de validação de sessão no middleware foi movida para verificação JWT pura (sem query de banco), e as queries pesadas de usuário foram migradas para Server Components (layout.tsx) onde o Node.js Runtime está disponível:

// middleware.ts ANTES (problemático)
import { validateSession } from '@/lib/auth'  // ← Arrasta PrismaClient para o Edge

// middleware.ts DEPOIS (correto)
import { jwtVerify } from 'jose'  // ← Pura criptografia, compatível com Edge

O triângulo da velocidade: por que o hardware legado sobreviveu#

Após o bun prisma generate e o reinício do servidor, a diferença de performance foi imediatamente perceptível. O HMR respondia em menos de 200ms para mudanças de componente. O que tecnicamente explica isso em hardware com apenas 2 cores físicos:

Vértice 1 - Bun (runtime em zig)#

O Node.js, por design, carrega o runtime V8 com todas as suas validações de segurança, JIT pipeline completo e ciclo de inicialização pesado. O Bun, escrito em Zig, usa o motor JavaScript JavaScriptCore (o engine do Safari/WebKit) e elimina camadas inteiras de overhead no startup:

Node.js startup sequence:
  V8 heap initialization (~60-80ms)
  → Module resolver bootstrap
  → Native bindings loader
  → Event loop initialization
  Total: ~150-300ms por processo filho

Bun startup sequence:
  JavascriptCore initialization (~5-10ms)
  → Direct module resolution (no CommonJS negotiation)
  → Native API layer (Zig, sem ABI overhead do N-API)
  Total: ~10-30ms por processo filho

Em um dual-core onde cada ms de CPU é precioso durante re-compilação incremental, a diferença acumula.

Vértice 2 - turbopack (bundler em Rust)#

O Webpack (bundler padrão do Next.js antes do Turbopack) é single-threaded em JavaScript. Num dual-core, ele monopoliza um thread para transformações de módulo e o outro thread do OS fica sub-utilizado.

O Turbopack é multithread nativo em Rust, utilizando rayon para paralelismo de trabalho. Num dual-core + HyperThreading (4 threads lógicos), o Turbopack distribui a compilação de forma que todos os 4 threads lógicos contribuem:

Webpack em dual-core HT:
  Thread 0 (físico):  ████████████████ 100% bundle work
  Thread 1 (físico):  ████             ~25% (I/O wait)
  Threads HT:         ░░░░░░░░         ~5% utilização

Turbopack em dual-core HT:
  Thread 0 (físico):  ████████████████ 100% (módulos A-M)
  Thread 1 (físico):  ████████████████ 100% (módulos N-Z)
  Threads HT:         ██████████       ~65% (tarefas menores)

O consumo de RAM do Turbopack para este projeto ficou entre 800 MB e 1.4 GB, contra 2.1 a 3.2 GB do Webpack equivalente no mesmo projeto. Com 10 GB de RAM disponíveis, a diferença de 1+ GB muda completamente a dinâmica de swapping (especialmente considerando os ajustes de vm.swappiness=10 já aplicados neste host).

Vértice 3 - prisma query engine (binário em Rust)#

O Prisma Engine é compilado nativamente para a arquitetura do host. Num i5 Haswell de 4ª geração, o binário libquery_engine-linux-musl.so.node executa as queries no processador sem camada de interpretação JavaScript:

ORM JavaScript puro (ex: Knex + driver pg):
  JS → V8 JIT → bindings N-API → driver TCP → banco

Prisma Client:
  JS → PrismaClient.findMany() → IPC → Query Engine (Rust nativo) → banco

A diferença é mais perceptível em queries com joins complexos ou mutations em lote: o Rust elimina a pressão de GC (Garbage Collector) do V8 durante operações de banco intensivas.

Auditoria de compatibilidade: as 3 zonas de risco no package.json#

Com o ambiente funcionando, auditei cada dependência pesada para classificar o risco de quebra no Bun:

Zona verde - compatibilidade total#

@radix-ui/*          → Pure JS/React, zero bindings nativos
lucide-react         → SVG components, Pure TypeScript
zod                  → Pure TypeScript, validação em JS puro
framer-motion        → Pure JS com Web Animations API
@dnd-kit/core        → Pure JS, pointer events API
@tiptap/react        → Pure TypeScript
bcryptjs             → Implementação JS pura (vs. `bcrypt` em C++ que teria risco)
tailwindcss v4       → CSS toolchain, CLI separada do runtime

Nota crítica sobre bcryptjs vs bcrypt: A escolha deliberada de bcryptjs em vez de bcrypt é arquiteturalmente correta para compatibilidade com Bun. A lib bcrypt (sem "js") usa node-gyp para compilar bindings C++. O Bun tem compatibilidade parcial com node-gyp, mas resulta em falhas intermitentes em arquiteturas Haswell. O bcryptjs roda em JS puro - 3-4x mais lento em hash computation, mas universalmente compatível.

Zona amarela - compatibilidade com ressalvas#

nodemailer           → Usa módulos `net` e `tls` do Node.js
auth.js v5           → Usa `crypto` do Node para cookies de sessão
@clerk/nextjs        → SDK pesado, alguns módulos com Node APIs

O Bun implementa uma camada de compatibilidade (Bun.polyfill) para net, tls e crypto do Node.js. Na prática, o envio de e-mail via SMTP com nodemailer funcionou corretamente nos testes, mas requer validação em ambiente de staging antes de ir para produção, especialmente com conexões TLS onde o comportamento de handshake pode diferir.

Zona vermelha - risco alto de quebra#

@react-pdf/renderer:

Risco: ALTO
Razão: Depende de `zlib` e `streams` com comportamento específico do Node.js
   A implementação de streams do Bun é parcial (v1.1.x ainda tem gaps)
   O canvas rendering interno usa `buffer` operations pesadas
Sintoma esperado: Timeout ou corrupção de buffer em PDFs com imagens
Mitigação: Isolar geração de PDF em uma API Route com runtime explícito:
  → export const runtime = 'nodejs'

fabric (canvas server-side):

Risco: MUITO ALTO
Razão: Depende de `node-canvas` que compila C++ via node-gyp
   node-canvas requer Cairo, Pango, libjpeg instalados no sistema
   O binding compilation do Bun para node-gyp é experimental
Sintoma esperado: Falha na instalação ou SIGSEGVs em runtime
Mitigação: Usar fabric apenas no client-side (browser) via dynamic import:
  → const { fabric } = await import('fabric') apenas em componentes client

Estratégia híbrida: Bun para dev, Node.js para casos específicos#

A conclusão operacional não é "Bun ou Node.js": é Bun como default, Node.js como escape hatch:

# Desenvolvimento diário - Bun (velocidade máxima)
bun dev
bun add <package>
bun prisma generate

# Teste isolado de rota problemática (ex: PDF generation)
NODE_OPTIONS='--max-old-space-size=4096' node --experimental-vm-modules \
  node_modules/.bin/next dev -H 127.0.0.1 --port 3001

# CI/CD - manter Node.js como runtime de build (mais previsível)
# package.json scripts com fallback explícito:
{
  "scripts": {
"dev": "bun run next dev",
"build": "node node_modules/.bin/next build",
"build:bun": "bun run next build",
"test:pdf": "node scripts/test-pdf-generation.js"
  }
}

Métricas observadas no hardware legado#

Operaçãopnpm + Node.jsBun + TurbopackGanho
install (limpo)~45s~13s3.5x
install (cache)~8s~4s2x
dev startup~12s~4s3x
HMR (componente simples)~800ms~150ms5x
HMR (page com imports pesados)~2.4s~600ms4x
RAM em idle (dev rodando)~2.8 GB~1.3 GB-54%

Este artigo foi útil?

Deixe uma reação rápida para apoiar o conteúdo:

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários