Estágio 02 · 02-07
LockedA maior parte dos devs JS que escrevem servidor em Node sabe quase nada do runtime. Funciona até a primeira vez que: o servidor congela em produção sob carga; uma stream consome memória e mata o container; um middleware async faz timing virar nondeterminístico; um child process zumbi mantém o processo vivo. Aí "Node é um runtime de JS" não basta.
Este módulo é dissecação. V8, libuv, event loop com suas seis fases, microtasks, streams, buffers, workers, child processes, cluster, signals, exit codes, debugging com --inspect. Você sai daqui sabendo o que node app.js realmente faz.
Node é runtime de JS server-side construído sobre:
fs, net, crypto, http, etc. são wrappers JS sobre código C++ que chama libuv ou syscalls.Ao rodar node app.js:
V8 compila JS pra bytecode (Ignition) e re-otimiza hot code pra machine code (TurboFan). GC é generational (Scavenger pra young, Mark-Sweep-Compact pra old).
Implicações práticas:
--max-old-space-size=4096.libuv é organizada em fases:
setTimeout/setInterval cujo tempo expirou.setImmediate.socket.on('close') etc.Entre cada fase, Node drena:
.then, queueMicrotask).process.nextTick (fila própria do Node, drenada antes de microtasks).Ordem comum (após código síncrono terminar):
Por isso process.nextTick em loop infinito bloqueia o event loop inteiro: ele drena antes de qualquer fase rodar.
console.log('1');
setTimeout(() => console.log('2'), 0);
setImmediate(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
process.nextTick(() => console.log('5'));
console.log('6');
Output: 1 6 5 4 2 3 (na maioria dos cenários). Por quê:
(Ordem entre setTimeout 0 e setImmediate é menos determinística fora de I/O, em I/O callback, setImmediate sempre vence.)
Algumas operações bloqueariam o thread JS, então libuv as offload pra thread pool:
fs.* (a maioria, exceto fs.watch).dns.lookup (não os outros DNS APIs).crypto.pbkdf2, crypto.scrypt etc.zlib (compressão).Default: 4 threads. Variável: UV_THREADPOOL_SIZE (até 1024). Aumentar ajuda CPU-bound APIs em paralelo, mas só faz sentido se você tem CPUs.
I/O de rede (TCP, UDP) não usa thread pool: usa kernel async (epoll/kqueue/IOCP). Por isso Node escala bem em rede mesmo com pool default pequeno.
Buffer é a estrutura de Node pra dados binários. Wrap de Uint8Array com APIs extras (toString('hex'), etc.).
Buffer.alloc(n), zerado, seguro.Buffer.allocUnsafe(n), não zerado, mais rápido, mas pode vazar memória de processos anteriores se você não sobrescrever.Buffer.from(string, encoding), converte.Em código moderno, prefira Uint8Array quando interop com Web Standard. Buffer quando precisa de APIs específicas.
Streams são abstração de fluxo de dados em Node. Tipos:
fs.createReadStream, HTTP request).fs.createWriteStream, HTTP response).zlib.createGzip).Modos:
data events.read(). Default antes de subscriber.Backpressure: se Writable não acompanha Readable, write() retorna false. Stream pipeline respeita isso. Se você não respeitar, memória acumula.
API moderna: pipeline:
import { pipeline } from 'stream/promises';
await pipeline(
fs.createReadStream('input.csv'),
csvParser(),
transform,
fs.createWriteStream('output.json')
);
Trata erros, fecha streams corretamente, propaga backpressure.
Async iteration:
for await (const chunk of stream) { ... }
Streams Readable são async iterables.
process global expõe runtime info: argv, env, cwd(), pid, platform, version, memoryUsage(), cpuUsage().
Eventos importantes:
'exit', quando event loop esvazia.'beforeExit', antes de exit, ainda dá pra agendar trabalho.'uncaughtException', última chance antes de crashar (você deve logar e sair, não recuperar; estado do app pode estar corrompido).'unhandledRejection', promise sem .catch. Em Node 15+ default é abortar processo.child_process:
spawn(cmd, args), processo separado, stdio em streams. Use pra processos longos.exec(cmd), stdout/stderr em buffer (limite default ~1 MB; cuidado).fork(file), fork especializado pra outro Node script, com canal IPC (process.send).Cada processo tem PID separado, memória separada, kernel scheduler decide.
worker_threads (estável desde Node 12) dá threads JS reais com isolated V8 isolates, mas mesmo processo. Comunicam via MessageChannel/postMessage (structured clone) ou SharedArrayBuffer.
Use cases:
Diferença de cluster: cluster é multi-processo, worker_threads é multi-thread no mesmo processo (compartilha memória via SharedArrayBuffer, não via heap normal).
Módulo cluster permite forkar múltiplos workers Node, cada um sendo processo separado, todos compartilhando porta via master que faz round-robin.
Permite usar todos os cores em servidor de N requests por segundo. Cada worker tem heap próprio (não compartilha state em memória).
Em prática moderna: PM2, Node cluster nativo, ou apenas Docker rodando N réplicas. Em platforms de serverless/edge, irrelevante.
Linux/Mac mandam signals: SIGINT (Ctrl+C), SIGTERM (kill, docker stop), SIGKILL (não dá pra catch).
Server bem comportado:
SIGTERM.Em Node:
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
Pra HTTP/Express: server.close espera conexões keep-alive, pode ser necessário forçar timeout. Libs como stoppable ajudam.
CommonJS (CJS): require, module.exports. Síncrono. Default histórico.
ECMAScript Modules (ESM): import/export. Async loading. Default Web. Suportado em Node 14+ via .mjs ou "type": "module" em package.json.
Diferenças sutis:
await.__dirname, __filename não existem em ESM (use import.meta.url).Em 2026, projetos novos: ESM. Bibliotecas: dual publish (CJS + ESM) ainda comum.
package.json: metadata, deps, scripts, exports.
package-lock.json (npm) / yarn.lock / pnpm-lock.yaml / bun.lockb: pinned versions, garantindo reprodutibilidade.
exports field controla o que e como o pacote expõe (CJS, ESM, types). Substituiu o main antigo. Sem entender exports, você não publica pacote correto.
Bun, pnpm são alternativas com workspace e velocidade de install superiores. Para monorepo: pnpm workspaces ou Turborepo + Nx pra orquestrar.
AsyncLocalStorage (estável Node 16+) cria "thread-local" storage por contexto async. Útil pra request-scoped data (request id, user, tenant) sem passar argumento por todo lugar.
import { AsyncLocalStorage } from 'async_hooks';
const als = new AsyncLocalStorage();
app.use((req, res, next) => {
als.run({ requestId: crypto.randomUUID() }, () => next());
});
logger.info({ requestId: als.getStore()?.requestId }, 'something');
Custo: pequeno overhead em cada async boundary. Em prod, vale.
Padrões e armadilhas:
try/catch só pega throws síncronos ou em await. Promise sem await rejeitada vai pra unhandledRejection.(err, data) => {}. Esquecer de checar err é bug clássico.next(err). Express 5 (estável agora) trata async automaticamente.--inspect / --inspect-brk: abre debugger compatível com Chrome DevTools.process.memoryUsage(), --heap-prof flag pra heap snapshot.--prof pra V8 sampling profiler (output em isolate-*.log).0x, flame graph generator.--async-stack-traces default), stack inclui caminho cross-await.Sintomas comuns:
clinic doctor ou medir event-loop-lag.Ctrl+C → handle/refs ainda abertos. process._getActiveHandles() debug.Os três rodam JS/TS server-side mas diferem em decisões de design, runtime base, e maturidade de ecossistema. Senior real escolhe consciente, não por moda.
| Dimensão | Node 22 LTS | Bun 1.x | Deno 2.x |
|---|---|---|---|
| Engine | V8 (Chrome) | JavaScriptCore (Safari) | V8 (Chrome) |
| I/O backbone | libuv | Bun's own (zig + io_uring quando disponível) | Tokio (Rust) |
| TS nativo | Não (precisa loader/--experimental-strip-types em 22.6+) | Sim, sem config | Sim, sem config |
| Package manager | npm/pnpm/yarn | bun install (CAS local, ~10x mais rápido que npm) | npm registry + deno install |
| Imports | CommonJS + ESM | CommonJS + ESM + auto-resolve .ts | ESM-only, URL imports + npm: specifier |
| Bundler builtin | Não (use esbuild/swc) | Sim (bun build) | Sim (deno bundle deprecated; deno compile) |
| Test runner | node --test (built-in desde 18) | bun test (Jest-compat API) | deno test (built-in) |
| Native code | N-API, addon C++ | N-API parcial + Bun.FFI | FFI nativo (Deno.dlopen) |
| Permissions | Sem (full access) | Sem (full access) | Granular (--allow-net, --allow-read, etc.) |
| Maturidade prod | Total (10+ anos) | Crescendo (early adopters em prod desde 2024) | Estável em edge/scripts |
| Memory baseline | ~30MB idle | ~25MB idle | ~40MB idle |
| Startup time | ~50ms (TS via tsx) | ~5-10ms | ~25ms |
| Hot reload | --watch | --watch (mais rápido) | --watch |
Quando escolher Node:
Quando escolher Bun:
bun test é 5-10x mais rápido que Jest.bun --hot reinicia em milissegundos vs segundos.node: modules ~95%, testar caso específico.Quando escolher Deno:
Pegadinhas reais:
__dirname/CommonJS quebram. ESM-only é decisão consciente.Veredicto pragmático 2026:
Você precisa, sem consultar:
pipeline resolve.AsyncLocalStorage resolve e seu custo.Construir o Logística API v0: backend simples mas com diagnóstico forte.
http direto (vamos abstrair em 02-08).POST /orders cria pedido.GET /orders lista pedidos paginados.GET /orders/:id detalhe.POST /orders/:id/events adiciona evento (status update).GET /orders/:id/stream retorna stream NDJSON dos eventos do pedido (resposta gerada via Readable).GET /orders/export.csv deve gerar CSV streaming via pipeline sem carregar tudo em memória, mesmo com 100k pedidos.POST /reports/heatmap recebe range de datas, dispara cálculo CPU-bound (simule com crypto.scrypt ou loop matemático) em worker thread, retorna 202 com job id, e cliente faz polling em GET /reports/:id até pronto.SIGTERM para aceitar conexões, espera ≤ 10s pelas em curso, fecha DB pool, sai com 0.requestId (UUID).requestId em toda linha.GET /healthz retorna 200 se Postgres responde a SELECT 1.GET /metrics (texto Prometheus simples) com:
event_loop_lag_seconds (medido com monitorEventLoopDelay ou hr time loop).process_resident_memory_bytes.http_requests_total{route, status}.child_process.exec.clinic doctor durante load test (ex: autocannon), anotar event loop lag, latência p50/p99.kill -TERM <pid> durante request em curso, comportamento.MessageChannel entre worker thread e main pra job progress.--prof e converta em flame graph.http que vimos aqui.pg lib usa libuv/network; pool de conexões em event loop.net/http upgrade.Destrava
02-07 é prereq dos seguintes módulos: