
Node.js para desenvolvedores: event loop, módulos e performance em 2026
Conteúdo educativo. Exemplos testados em Node.js 22 LTS (Iron). Conceitos válidos para Node.js 20+ e Bun/Deno quando equivalentes. Métricas de benchmark variam por hardware.
Node.js não é um framework. É um runtime JavaScript construído sobre o motor V8 do Chrome, com uma biblioteca de eventos não-bloqueante (libuv) que permite milhares de conexões simultâneas em uma única thread. Criado em 2009 por Ryan Dahl, tornou-se a plataforma padrão para back-end JavaScript — mas ainda é mal compreendida por quem acha que 'tudo é assíncrono por padrão'.
A event loop: como Node.js faz mil coisas em uma thread
O segredo do Node.js é delegar operações lentas (IO de rede, filesystem, timers) para a libuv e continuar processando JavaScript na thread principal. Quando a operação termina, o callback entra em uma fila (task queue / microtask queue) e a event loop o consome quando a call stack estiver vazia.
- timers: setTimeout/setInterval callbacks.
- pending callbacks: IO callbacks deferred da fase anterior.
- idle/prepare: uso interno da libuv.
- poll: busca novos eventos de IO; executa callbacks de IO; pode esperar aqui.
- check: setImmediate callbacks.
- close callbacks: callbacks de socket.on('close') e similares.
Microtasks (Promise.then/catch/finally, queueMicrotask) têm prioridade sobre macrotasks. O que significa: um loop infinito de promises pode bloquear a event loop — não são mágicas.
// demonstração de ordem de execução
console.log('1. script start');
setTimeout(() => console.log('2. setTimeout'), 0);
setImmediate(() => console.log('3. setImmediate'));
Promise.resolve().then(() => console.log('4. promise microtask'));
process.nextTick(() => console.log('5. nextTick'));
console.log('6. script end');
// Saída real em Node.js:
// 1. script start
// 6. script end
// 5. nextTick (prioridade máxima, mas não é padrão ECMAScript)
// 4. promise microtask
// 2. setTimeout (pode vir antes do setImmediate dependendo do contexto)
// 3. setImmediateCommonJS vs ESM: a transição que ainda dói
CommonJS (require/module.exports) é o sistema legado. ESM (import/export) é o padrão ECMAScript. Node.js 22 suporta ambos, mas a interoperabilidade tem arestas: ESM não pode usar require() síncrono sem createRequire, e CommonJS pode importar ESM apenas via import() dinâmico (assíncrono).
| Aspecto | CommonJS | ESM |
|---|---|---|
| Sintaxe | require / module.exports | import / export |
| Carregamento | Síncrono, runtime | Assíncrono, análise estática |
| Top-level await | Não | Sim |
| __dirname / __filename | Disponível | Não disponível (use import.meta.url) |
| Cache de módulo | Sim, por caminho resolvido | Sim, por URL |
| JSON import | require('./config.json') | import config from './config.json' assert { type: 'json' } (ou importAttributes em 22+) |
Recomendação para projetos novos: use ESM ("type": "module" no package.json). Para bibliotecas, ofereça dual package (CJS + ESM) via conditional exports no package.json. Ferramentas como tsup e unbuild automatizam isso.
HTTP nativo e quando usar Express
O módulo http nativo é poderoso e zero-dependência, mas verboso para rotas complexas. Use-o para proxies simples, health checks ou quando cada megabyte de node_modules importa (AWS Lambda com cold start sensível). Express/Fastify/Koa entram quando você precisa de middleware, parsing de body, routing parametrizado e tratamento de erro estruturado.
// HTTP nativo — servidor mínimo
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.url === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, uptime: process.uptime() }));
return;
}
res.writeHead(404);
res.end('not found');
});
server.listen(3000, () => console.log('Listening on :3000'));// Fastify — mais rápido que Express, schema-first, menor overhead
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get('/health', async () => ({ ok: true }));
app.post('/echo', {
schema: {
body: {
type: 'object',
properties: { msg: { type: 'string' } },
required: ['msg'],
},
},
}, async (request) => ({ echoed: request.body.msg }));
await app.listen({ port: 3000 });Streams: processar dados sem carregar tudo na memória
Streams são sequências de dados processadas aos pedaços. Em vez de ler um arquivo de 2 GB para um Buffer, você processa chunk por chunk. Node.js implementa quatro tipos: Readable, Writable, Duplex e Transform. A API pipeline (node:stream/promises) gerencia erros e encerramento automaticamente.
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGunzip } from 'node:zlib';
// descomprimir arquivo grande sem carregar na RAM
await pipeline(
createReadStream('dados.json.gz'),
createGunzip(),
createWriteStream('dados.json')
);
// processar linha a linha com Transform
import { Transform } from 'node:stream';
const toUpper = new Transform({
transform(chunk, _encoding, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
await pipeline(
process.stdin,
toUpper,
process.stdout
);Async/await, promises e os anti-padrões comuns
Async/await é açúcar sintático sobre promises. O problema é que erros em async functions sem try/catch viram rejections não tratadas, que no Node.js 22+ terminam o processo (uncaughtException). Sempre envolva operações externas em try/catch ou use .catch() no nível adequado.
// anti-padrão: await em loop síncrono (piora performance)
for (const id of ids) {
await fetchUser(id); // executa sequencialmente
}
// melhor: paralelismo controlado
const users = await Promise.all(ids.map(id => fetchUser(id)));
// melhor ainda: com limite de concorrência (p-map, async-pool)
import pMap from 'p-map';
const users = await pMap(ids, fetchUser, { concurrency: 5 });Outro erro comum: não tratar rejections de Promise.all. Se uma promise rejeita, todas as outras continuam rodando, mas você perde o resultado. Use Promise.allSettled quando precisar dos resultados independentemente de falhas parciais.
Gerenciamento de pacotes: npm, yarn, pnpm, corepack
npm é o padrão, mas pnpm resolve o problema de disco duplicado (node_modules monolíticos) usando hard links e content-addressable store. Yarn 4 (Berry) usa Plug'n'Play, eliminando node_modules por completo. Corepack (incluído no Node.js 18+) permite usar yarn/pnpm sem instalá-los globalmente.
| Ferramenta | Diferencial | Quando usar |
|---|---|---|
| npm (v10) | Padrão, zero config | Projetos pequenos, CI genérico |
| pnpm | Economia de disco, lockfile determinístico | Monorepos, muitas dependências |
| Yarn 4 | PnP, constraints, melhor cache | Times que já usam Yarn |
| Bun | Runtime + bundler + package manager | Prototipagem rápida (instalação ~30x mais rápida) |
Dica prática: fixe a versão do gerenciador no package.json ("packageManager": "pnpm@9.15.0+sha256...") para que CI e colaboradores usem exatamente a mesma ferramenta e versão. Corepack lê esse campo automaticamente.
Debugging e profiling de memória
O inspector nativo do Node.js expõe o protocolo Chrome DevTools. Use --inspect para depurar com breakpoints no VS Code ou Chrome. Para vazamentos de memória, gere heap snapshots com --heapsnapshot-near-heap-limit e analise no Chrome DevTools Memory tab — procure por objetos que crescem monotonicamente entre snapshots.
# iniciar com inspector
node --inspect=0.0.0.0:9229 server.js
# profiling de CPU (gera arquivo .cpuprofile para DevTools)
node --cpu-prof server.js
# heap snapshot automático antes de OOM
node --heapsnapshot-near-heap-limit=3 server.jsClosures acumulativas são a fonte mais comum de memory leak em Node.js: um callback armazena referência a um objeto grande que nunca é liberado porque o event emitter ainda existe. Use WeakRef e FinalizationRegistry com cuidado — são ferramentas avançadas, não cura para design ruim.
Ambiente de produção: o que muda
- Nunca rode Node.js como root. Use um usuário dedicado (node, app) ou container com USER.
- Gerenciadores de processos: PM2 é prático, mas systemd ou containers (Docker, Kubernetes) são mais robustos para restart, logs e health checks.
- Variáveis de ambiente: nunca commite .env. Use dotenv só em desenvolvimento; em produção, injete via plataforma (Render, Fly, AWS Secrets Manager).
- Graceful shutdown: intercepte SIGTERM para fechar conexões de banco e servidores HTTP antes de sair. SIGKILL (kill -9) não pode ser interceptado — seu processo morre no meio de uma transação.
- NODE_ENV=production ativa otimizações do V8 e simplifica logs de erro (não expõe stack traces detalhadas ao cliente).
- Monitore event loop lag: bibliotecas como toobusy-js ou métricas do event loop delay (perf_hooks) indicam quando a thread principal está saturada.
// graceful shutdown mínimo
import { createServer } from 'node:http';
const server = createServer(handler);
server.listen(3000);
function shutdown() {
console.log('SIGTERM recebido. Fechando servidor...');
server.close(() => {
console.log('Servidor fechado. Saindo.');
process.exit(0);
});
// fallback se demorar muito
setTimeout(() => process.exit(1), 10000);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);Perguntas frequentes
+Node.js é single-threaded? Como escala?
A thread principal de JavaScript é single-threaded, mas a libuv usa um pool de threads para operações de sistema (IO, criptografia, compressão). Para escalar CPU, use cluster (múltiplos processos Node.js compartilhando a porta) ou Worker Threads (threads isoladas para tarefas pesadas). Em produção, o mais comum é rodar múltiplas instâncias atrás de um load balancer (Nginx, HAProxy) ou orquestrador.
+Bun ou Deno vão substituir o Node.js?
Não em curto prazo. Bun é incrivelmente rápido (runtime, bundler, test runner), mas ainda amadurecendo em compatibilidade de APIs nativas. Deno tem segurança por padrão (permissions) e TypeScript nativo, mas o ecossistema npm é menor. Node.js 22+ continua sendo a escolha segura para produção por causa do ecossistema maduro, LTS previsível e adoção corporativa massiva.
+TypeScript ou JavaScript puro em projetos Node.js?
TypeScript para qualquer projeto com mais de um desenvolvedor ou com vida útil prevista acima de 6 meses. A tipagem estática pega erros em tempo de compilação, melhora a DX com autocomplete e documenta contratos de API. Use tsx ou ts-node em dev; compile para JS em produção (tsc, esbuild, swc) para evitar overhead de transpilação no runtime.
+Como escolher entre Express e Fastify?
Express é o padrão de fato, com milhares de middlewares e maior base de código legada. Fastify é mais rápido (~20% menor latência), schema-first (validação automática via JSON Schema), e melhor em logs estruturados. Para projetos novos, Fastify é a recomendação; para manutenção de código legado, Express é o caminho de menor resistência.
+O que é cluster mode e como ativar?
O módulo cluster do Node.js cria múltiplos processos worker que compartilham a mesma porta TCP. Cada worker roda em uma CPU core, superando o limite de uma única thread. PM2 faz isso automaticamente (pm2 start app.js -i max). Sem cluster, um servidor Node.js usa apenas um core, desperdiçando máquinas multi-core.
Fontes consultadas
- Node.js 22 Documentation
- libuv design overview (event loop)
- Fastify documentation
- ESM in Node.js (Joyee Cheung, Node.js blog)
- Node.js Best Practices (goldbergyoni)
Revisão editorial: publicado em . Última revisão em . Conteúdo educativo, sem patrocínio das ferramentas citadas.
Leia também

Como Começar na Programação do Zero em 2026: Guia Definitivo
Roteiro completo para quem quer entrar na programação em 2026: escolha de linguagem, setup de ambiente, lógica de programação, primeiros projetos, comunidades e como evitar armadilhas comuns de iniciantes.

HTML, CSS e JavaScript: O Trio Essencial da Web Moderna
Domine os três pilares do desenvolvimento web: HTML semântico para estrutura, CSS moderno para estilo e layout responsivo, e JavaScript para interatividade, manipulação do DOM e consumo de APIs.

Git e GitHub Básico: Controle de Versão para Quem Coda
Guia prático de Git e GitHub para iniciantes: instalação, commits, branches, merge, rebase, pull requests, resolução de conflitos e fluxos de trabalho profissionais em equipe.