icardb
Node.js para desenvolvedores: event loop, módulos e performance em 2026
Voltar para artigosPROGRAMAÇÃO

Node.js para desenvolvedores: event loop, módulos e performance em 2026

Por Equipe Editorial Icardb 7 min de leitura

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.

javascript
// 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. setImmediate

CommonJS 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).

AspectoCommonJSESM
Sintaxerequire / module.exportsimport / export
CarregamentoSíncrono, runtimeAssíncrono, análise estática
Top-level awaitNãoSim
__dirname / __filenameDisponívelNão disponível (use import.meta.url)
Cache de móduloSim, por caminho resolvidoSim, por URL
JSON importrequire('./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.

javascript
// 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'));
javascript
// 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.

javascript
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.

javascript
// 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.

FerramentaDiferencialQuando usar
npm (v10)Padrão, zero configProjetos pequenos, CI genérico
pnpmEconomia de disco, lockfile determinísticoMonorepos, muitas dependências
Yarn 4PnP, constraints, melhor cacheTimes que já usam Yarn
BunRuntime + bundler + package managerPrototipagem 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.

bash
# 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.js

Closures 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.
javascript
// 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

Revisão editorial: publicado em . Última revisão em . Conteúdo educativo, sem patrocínio das ferramentas citadas.

Crédito da imagem: Ilustração editorial: Equipe Icardb

Leia também