O que significa “executar JavaScript fora do navegador”
No Node.js, o JavaScript roda dentro do motor V8 (o mesmo que existe em navegadores), mas o ambiente ao redor é diferente: em vez de APIs de DOM e eventos do navegador, você tem APIs de sistema operacional (arquivos, rede, processos) expostas por módulos como fs, net, http e child_process. Esse “ambiente” é implementado em C/C++ e integra o V8 com o sistema operacional, principalmente por meio da biblioteca libuv.
Na prática, isso permite construir back-ends e CLIs com JavaScript/TypeScript, com um modelo de concorrência baseado em: (1) uma thread principal executando JavaScript, (2) um loop de eventos (event loop) e (3) um conjunto de threads auxiliares (thread pool) para certas operações.
Processo do Node.js: o que é e por que importa
Quando você executa node app.js, o sistema operacional cria um processo. Esse processo tem memória própria, descritores de arquivo, sockets, variáveis de ambiente e um identificador (PID). Dentro desse processo, o Node inicia o V8 e o loop de eventos.
Observando o processo em um script CLI
Crie um arquivo process-info.js:
console.log({ pid: process.pid, ppid: process.ppid, platform: process.platform });
console.log({ node: process.version, arch: process.arch });
console.log({ cwd: process.cwd() });
console.log({ memory: process.memoryUsage() });
Execute:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
node process-info.jsO objetivo aqui é reconhecer que cada execução é um processo isolado e que, em back-ends, você pode ter múltiplos processos (por exemplo, em cluster/containers) para escalar CPU e isolar falhas.
Thread principal, event loop e o modelo de concorrência
O Node.js é frequentemente descrito como “single-threaded”, mas isso é um atalho: o JavaScript roda em uma thread principal, porém o processo pode usar outras threads internamente (principalmente via libuv).
Thread principal (main thread)
- Executa seu código JavaScript.
- Se você fizer uma tarefa pesada de CPU nessa thread, você bloqueia o processamento de timers, callbacks e requisições.
- É por isso que “não bloquear o event loop” é um critério central no desenho de back-ends em Node.
Event loop (loop de eventos)
O event loop é um mecanismo que coordena: timers (setTimeout), I/O (rede/arquivos), callbacks, Promises/microtasks e outras fontes de eventos. A ideia é: você inicia operações assíncronas, e quando elas terminam, o loop agenda seus callbacks para execução na thread principal.
libuv e thread pool
libuv é a biblioteca que fornece abstrações multiplataforma para I/O assíncrono e o event loop. Além disso, ela mantém um thread pool usado para operações que não são naturalmente não-bloqueantes em todas as plataformas (por exemplo, várias operações de fs, DNS via certas APIs, compressão/crypto em alguns casos). Enquanto essas tarefas rodam no pool, a thread principal pode continuar atendendo eventos.
Um detalhe importante: o thread pool tem tamanho limitado (por padrão, 4). Se você disparar muitas tarefas que usam esse pool, elas entram em fila e podem aumentar latência. Você pode ajustar com UV_THREADPOOL_SIZE (com cuidado, porque aumentar demais pode causar contenção e piorar desempenho).
Impacto direto no desenho de back-ends
- Excelente para I/O: APIs HTTP, acesso a banco, cache, filas e chamadas a serviços externos são I/O-bound; o Node lida bem com muitas conexões simultâneas quando o trabalho por requisição é pequeno e assíncrono.
- Risco em CPU: processamento pesado (hashing em massa, compressão grande, parsing/transformações extensas, relatórios complexos) pode bloquear o event loop e degradar todo o servidor.
- Latência previsível: manter a thread principal livre tende a reduzir “picos” de latência (tail latency) em endpoints.
- Escala horizontal e isolamento: para CPU, é comum usar workers/filas ou separar em serviços dedicados.
Experimento 1: I/O assíncrono não bloqueia o loop
Vamos criar um script que faz uma leitura de arquivo assíncrona e, ao mesmo tempo, mantém um “batimento” (heartbeat) com setInterval para mostrar que o loop continua vivo.
Passo a passo
- Crie um arquivo grande para leitura (ex.: 200MB). Em macOS/Linux:
dd if=/dev/zero of=bigfile.bin bs=1m count=200No Windows (PowerShell), uma alternativa simples é criar um arquivo com tamanho fixo:
fsutil file createnew bigfile.bin 209715200- Crie
io-async.js:
const fs = require('fs');
const file = process.argv[2] || 'bigfile.bin';
let ticks = 0;
const start = Date.now();
const timer = setInterval(() => {
ticks++;
const elapsed = Date.now() - start;
console.log(`tick=${ticks} elapsed=${elapsed}ms`);
}, 200);
fs.readFile(file, (err, data) => {
if (err) throw err;
const elapsed = Date.now() - start;
console.log(`readFile done: bytes=${data.length} elapsed=${elapsed}ms`);
clearInterval(timer);
});
- Execute:
node io-async.js bigfile.binO que observar
- Os
tick=...continuam aparecendo enquanto o arquivo é lido. - Isso indica que a operação de I/O foi delegada (via libuv/OS) e o event loop continuou processando timers.
Experimento 2: I/O síncrono bloqueia o loop
Agora vamos repetir, mas com leitura síncrona, que roda na thread principal e bloqueia o event loop.
Passo a passo
- Crie
io-sync.js:
const fs = require('fs');
const file = process.argv[2] || 'bigfile.bin';
let ticks = 0;
const start = Date.now();
const timer = setInterval(() => {
ticks++;
const elapsed = Date.now() - start;
console.log(`tick=${ticks} elapsed=${elapsed}ms`);
}, 200);
// Bloqueia a thread principal
const data = fs.readFileSync(file);
const elapsed = Date.now() - start;
console.log(`readFileSync done: bytes=${data.length} elapsed=${elapsed}ms`);
clearInterval(timer);
- Execute:
node io-sync.js bigfile.binO que observar
- Os ticks não aparecem (ou aparecem só depois), porque o
readFileSyncimpede o loop de executar o callback dosetInterval. - Em um servidor HTTP, isso equivaleria a “congelar” o atendimento de outras requisições durante a operação.
Experimento 3: CPU-bound bloqueia mesmo com APIs assíncronas ao redor
Mesmo que você use APIs assíncronas para I/O, uma tarefa pesada de CPU no meio do caminho bloqueia o event loop. Vamos simular com um loop de cálculo.
Passo a passo
- Crie
cpu-block.js:
function burnCpu(ms) {
const end = Date.now() + ms;
let x = 0;
while (Date.now() < end) {
x = Math.sqrt(Math.random() * 1e9) + x;
}
return x;
}
let ticks = 0;
const start = Date.now();
setInterval(() => {
ticks++;
console.log(`tick=${ticks} elapsed=${Date.now() - start}ms`);
}, 200);
console.log('Starting CPU burn...');
const result = burnCpu(3000);
console.log('CPU burn done', { result, elapsed: Date.now() - start });
- Execute:
node cpu-block.jsO que observar
- Durante ~3s, os ticks param de aparecer.
- Isso é o sintoma clássico de endpoint CPU-bound em Node: o servidor fica “vivo”, mas não responde com baixa latência.
Experimento 4: medindo atraso do event loop (event loop lag)
Uma forma prática de detectar bloqueios é medir o atraso de timers. Se você agenda um timer para rodar a cada 100ms, mas ele roda com 500ms, algo bloqueou a thread principal.
Passo a passo
- Crie
event-loop-lag.js:
function burnCpu(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const interval = 100;
let expected = Date.now() + interval;
setInterval(() => {
const now = Date.now();
const lag = now - expected;
expected = now + interval;
console.log(`lag=${lag}ms`);
}, interval);
setTimeout(() => {
console.log('Blocking CPU for 2000ms...');
burnCpu(2000);
console.log('Done blocking');
}, 1000);
- Execute:
node event-loop-lag.jsO que observar
- Antes do bloqueio, o
lagtende a ficar baixo (varia com o sistema). - Durante o bloqueio, o
lagdispara (próximo de 2000ms). - Em back-ends, lag alto costuma se correlacionar com latência alta e timeouts.
Experimento 5: saturando o thread pool do libuv
Algumas operações assíncronas usam o thread pool. Se você disparar muitas ao mesmo tempo, elas competem por poucas threads e começam a enfileirar. Vamos observar isso com crypto.pbkdf2, que usa o pool.
Passo a passo
- Crie
threadpool-saturation.js:
const crypto = require('crypto');
const start = Date.now();
const tasks = Number(process.argv[2] || 8);
for (let i = 0; i < tasks; i++) {
crypto.pbkdf2('password', 'salt', 200000, 64, 'sha512', () => {
const elapsed = Date.now() - start;
console.log(`task ${i} done at ${elapsed}ms`);
});
}
- Execute com o padrão (pool ~4):
node threadpool-saturation.js 8- Agora execute aumentando o pool (ex.: 8):
UV_THREADPOOL_SIZE=8 node threadpool-saturation.js 8O que observar
- Com pool menor, as tarefas terminam em “ondas” (primeiro um grupo, depois outro), indicando fila.
- Com pool maior, pode haver mais paralelismo, mas nem sempre melhora: depende de CPU disponível e contenção.
Exercícios práticos (diagnóstico e interpretação)
1) Identificar tarefas bloqueantes
Objetivo: classificar cada trecho como “bloqueia o event loop” ou “não bloqueia”.
fs.readFileSync(...)fs.readFile(...)- Um loop
whileque roda por 2 segundos crypto.pbkdf2(..., callback)JSON.parsede um JSON muito grande (dezenas de MB)
Critério: se roda na thread principal por muito tempo, bloqueia; se delega e retorna rápido, tende a não bloquear (mas pode saturar pool).
2) Medir tempo de execução e comparar abordagens
Objetivo: medir e comparar readFile vs readFileSync e interpretar impacto no loop.
- Rode
io-async.jse anote: tempo total e quantos ticks ocorreram. - Rode
io-sync.jse anote: tempo total e quantos ticks ocorreram. - Explique em 2 frases por que os ticks mudaram.
3) Estimar o “custo” de CPU no seu ambiente
Objetivo: calibrar o que é “pesado” no seu computador/servidor.
- Altere
burnCpu(3000)para 100ms, 500ms, 1000ms, 3000ms. - Observe a diferença no
lagdo scriptevent-loop-lag.js. - Defina um limite prático: “acima de X ms de CPU por requisição, a latência começa a ficar inaceitável”.
4) Interpretar saturação do thread pool
Objetivo: entender fila vs paralelismo.
- Rode
threadpool-saturation.jscom 4, 8, 16 tarefas. - Compare o padrão de conclusão das tasks.
- Teste
UV_THREADPOOL_SIZEcom 4 e 8 e descreva quando melhora e quando piora.
Critérios objetivos para decidir: async, filas, workers ou serviços separados
Quando uma abordagem assíncrona (event loop) é suficiente
- A tarefa é majoritariamente I/O-bound (rede, banco, cache, disco) e o código retorna ao loop rapidamente.
- O tempo de CPU por requisição é baixo e estável (ex.: poucos ms a dezenas de ms) e o
event loop lagpermanece baixo sob carga. - Você consegue limitar concorrência de chamadas externas (ex.: não abrir milhares de conexões simultâneas sem controle).
Quando usar filas (background jobs)
- A tarefa não precisa responder no tempo da requisição (ex.: gerar relatório, enviar e-mails, processar imagens, conciliações).
- Você precisa de retries, backoff, idempotência e controle de throughput.
- O tempo de execução é alto ou variável e você quer proteger a latência do tráfego online.
Quando usar Workers (worker_threads) ou processos separados
- A tarefa é CPU-bound e consome tempo significativo (centenas de ms a segundos) e você quer manter o event loop responsivo.
- Você precisa paralelizar em múltiplos núcleos dentro do mesmo serviço (workers) ou com isolamento maior (processos).
- Você observou
event loop lagelevado durante picos e correlacionou com trechos CPU-bound.
Quando separar em um serviço dedicado
- O componente CPU-bound escala de forma diferente do restante (ex.: precisa de máquinas otimizadas para CPU/GPU).
- Você precisa de isolamento de falhas e deploy independente (um bug no processamento não pode derrubar a API).
- Há dependências específicas (bibliotecas nativas, runtime diferente) ou requisitos de segurança/limites de recursos.
- O volume/complexidade exige observabilidade e tuning próprios (fila, concorrência, rate limit, circuit breaker).
Tabela de decisão rápida
| Cenário | Sintoma | Abordagem recomendada |
|---|---|---|
| Muitas requisições leves com chamadas a DB/cache | CPU baixa, I/O alto | Assíncrono no event loop + limites de concorrência |
| Endpoint faz transformação pesada (CPU) | Lag alto, latência sobe para todos | Worker/processo para CPU-bound |
| Tarefa longa não precisa ser síncrona | Timeouts, picos de latência | Fila + worker de background |
| Processamento especializado e escalável separadamente | Deploy/escala conflitam com API | Serviço separado |
| Muitas operações que usam thread pool | Conclusão em ondas, fila interna | Ajustar concorrência, considerar pool/arquitetura |