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:
| Ambiente | Comportamento |
|---|---|
next dev local | Pode funcionar silenciosamente (Next.js simula parte do Edge em dev) |
| Vercel / Cloudflare Production | Falha 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ção | pnpm + Node.js | Bun + Turbopack | Ganho |
|---|---|---|---|
install (limpo) | ~45s | ~13s | 3.5x |
install (cache) | ~8s | ~4s | 2x |
dev startup | ~12s | ~4s | 3x |
| HMR (componente simples) | ~800ms | ~150ms | 5x |
| HMR (page com imports pesados) | ~2.4s | ~600ms | 4x |
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:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários