Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-07
BloqueadoA 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:
Event loop blocking é a causa #1 de p99 latency catastrófico em Node prod. Single CPU-bound task de 200ms congela TODAS connections do process. Sem detection, time só descobre via "site is slow" tickets — quando user já abriu Twitter pra reclamar. §2.18 cobre 4 fronts: (1) detect via monitorEventLoopDelay + APM, (2) offload via worker_threads (CPU work) ou cluster (multi-process), (3) tune libuv thread pool, (4) anti-patterns que matam silenciosamente.
Detecting blocking — monitorEventLoopDelay (Node 11+):
import { monitorEventLoopDelay } from 'perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
// Expor pra Prometheus
setInterval(() => {
prometheusGauge.set({
name: 'nodejs_event_loop_delay_ns',
}, {
p50: histogram.percentile(50),
p99: histogram.percentile(99),
max: histogram.max,
mean: histogram.mean,
});
histogram.reset();
}, 10_000);
resolution: 20: sample a cada 20ms. Lower = mais preciso, more overhead.Why it matters — single CPU work blocks I/O:
// BAD — 800ms de CPU bloqueia event loop por 800ms
app.get('/report', (req, res) => {
const data = getOrders();
const csv = data.map(o => `${o.id},${o.total}`).join('\n'); // CPU
for (let i = 0; i < 1_000_000; i++) {
processRow(data[i % data.length]); // CPU
}
res.send(csv);
});
// Durante esses 800ms: outros requests aguardam.
// Liveness probe não responde → K8s mata pod.
Fix 1: worker_threads pra CPU-bound work:
// workers/csv-export.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (orders) => {
const csv = orders.map(o => `${o.id},${o.total}`).join('\n');
parentPort.postMessage(csv);
});
// main.ts
import { Worker } from 'worker_threads';
import { Piscina } from 'piscina'; // pool de workers
const pool = new Piscina({
filename: new URL('./workers/csv-export.js', import.meta.url).pathname,
minThreads: 2,
maxThreads: 8,
idleTimeout: 30_000,
});
app.get('/report', async (req, res) => {
const orders = await getOrders();
const csv = await pool.run(orders); // event loop livre durante CPU work
res.send(csv);
});
postMessage (structured clone). Custo de serialization NÃO vale pra payloads gigantes (> 10MB).SharedArrayBuffer: zero-copy compartilhamento entre threads. Útil pra image/video processing.Fix 2: cluster pra paralelismo de I/O:
import cluster from 'cluster';
import os from 'os';
if (cluster.isPrimary) {
const workers = process.env.NODE_WORKERS ? parseInt(process.env.NODE_WORKERS) : os.cpus().length;
for (let i = 0; i < workers; i++) cluster.fork();
cluster.on('exit', (worker, code, signal) => {
log.warn({ pid: worker.process.pid, code, signal }, 'worker died, restarting');
cluster.fork();
});
} else {
await import('./server.js');
}
worker_threads vs cluster — decision:
| Workload | Use |
|---|---|
| CPU-bound (ML inference, image processing, crypto) | worker_threads + Piscina |
| I/O-bound (HTTP, DB) escalando além de single core | cluster (não-K8s) ou HPA (K8s) |
| Mixed: API servidor com hot path CPU ocasional | cluster + worker_threads (paralelos) |
| Native binding bloqueante | worker_threads (isolar) |
libuv thread pool tuning:
Default UV_THREADPOOL_SIZE = 4. Operations que usam: fs.*, dns.lookup (sync resolver), crypto.pbkdf2, crypto.scrypt, zlib.*.
100 simultaneous fs reads + 4 threads = 96 esperando.
Tune:
UV_THREADPOOL_SIZE=16 node server.js
Pegadinha: aumentar UV_THREADPOOL_SIZE desperdiça RAM se workload não é I/O fs/crypto. Validate com monitorEventLoopUtilization + thread pool saturation metrics.
monitorEventLoopUtilization (Node 14.10+):
import { performance } from 'perf_hooks';
let prev = performance.eventLoopUtilization();
setInterval(() => {
const next = performance.eventLoopUtilization();
const delta = performance.eventLoopUtilization(next, prev);
// delta.utilization 0-1 (% tempo busy)
prev = next;
prometheusGauge.set('nodejs_event_loop_utilization', delta.utilization);
}, 10_000);
Diagnostic toolkit — production debugging:
# CPU profile pra detectar hot path
node --cpu-prof --cpu-prof-dir=./profiles server.js
# Analyze: chrome://tracing or speedscope
# Heap snapshot
node --heapsnapshot-signal=SIGUSR2 server.js
# SIGUSR2 → dumps heap; analyze com Chrome DevTools
# Inspector remoto (cuidado em prod)
node --inspect=0.0.0.0:9229 server.js
# Trace events (low overhead)
node --trace-events-enabled --trace-event-categories=v8,node server.js
Logística stack pragmático:
// server.ts
import 'dotenv/config';
import { monitorEventLoopDelay, performance } from 'perf_hooks';
import { Piscina } from 'piscina';
import Fastify from 'fastify';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
const cpuPool = new Piscina({
filename: new URL('./workers/cpu-tasks.js', import.meta.url).pathname,
minThreads: 2,
maxThreads: 8,
});
const app = Fastify();
app.get('/healthz', async () => ({ ok: true }));
app.get('/metrics', async () => {
const elu = performance.eventLoopUtilization();
return {
eventLoopDelayP99Ms: histogram.percentile(99) / 1e6,
eventLoopDelayMaxMs: histogram.max / 1e6,
eventLoopUtilization: elu.utilization,
};
});
app.post('/cpu-heavy', async (req) => {
return cpuPool.run(req.body); // offload
});
await app.listen({ port: 8080, host: '0.0.0.0' });
Anti-patterns observados:
bcrypt.hashSync em handler: bloqueia event loop ~100ms por hash. Use async bcrypt.hash ou worker pool.JSON.parse em payload de 10MB: 50-100ms blocking. Use streaming parser (JSONStream).arr.map(heavyFn) com 100k items. Break com setImmediate ou worker.monitorEventLoopDelay: blocking só descoberto após "site is slow" tickets.UV_THREADPOOL_SIZE=64 random: sem medição da saturação real, só aumenta RAM e contention.Worker recriado a cada request: spawn cost ~50ms; use Piscina pool.Cruza com 02-07 §2.4 (event loop foundation), 02-07 §2.10 (cluster basics), 02-07 §2.16 (diagnóstico produção), 04-09 §2.20 (backpressure relaciona com saturation), 03-07 (observability captura ELU).
Profiling sem método é teatro. Sem método, abre Chrome DevTools, screenshot do flame, e "otimiza" função que consumia 0.3% do tempo. §2.19 cobre stack 2026: clinic.js 13+ (Doctor → categoriza problem antes de mergulhar), 0x 5+ (flamegraph standalone), V8 deopt tracing (perda silenciosa de JIT), heap snapshots comparison (leak detection real), Pyroscope 0.21+ (continuous prod profiling). Node 22 LTS assumido.
Stack 2026 — escolha por sintoma:
.cpuprofile / .heapprofile.clinic.js workflow — Doctor primeiro, sempre:
# Step 1: Doctor categoriza problem (EventLoop / GC / IO / CPU)
npx clinic doctor --on-port 'autocannon -d 30 -c 100 http://localhost:3000/orders' -- node server.js
# Step 2 (se CPU-bound): Flame pra detalhe de CPU
npx clinic flame --on-port 'autocannon -d 30 -c 100 http://localhost:3000/orders' -- node server.js
# Step 3 (se async chain confuso): Bubbleprof
npx clinic bubbleprof --on-port 'autocannon -d 30 -c 100 http://localhost:3000/orders' -- node server.js
# Step 4 (se memory growth): Heap
npx clinic heap --on-port 'autocannon -d 30 -c 100 http://localhost:3000/orders' -- node server.js
0x — flamegraph standalone:
npx 0x -- node server.js
# Em outro terminal: rode load (autocannon, k6, vegeta)
# Ctrl+C no 0x; output HTML flamegraph abre em browser
# Variant: profile já-rodando-em-background
npx 0x -P 'autocannon -d 20 -c 50 http://localhost:3000' -- node server.js
V8 deoptimization — JIT silenciosamente desistindo:
V8 JIT-compila funções hot via TurboFan. Se assumption de tipo é violada, V8 deopta pra interpretador — mesma função, 10-100× mais lenta, sem warning.
Causas comuns:
try/catch em hot loop (pre-Node 14; mitigado em Node 14+ mas ainda penaliza inlining).arguments object usage (use rest ...args).with, eval, mutação de prototype em runtime.# Trace deopts em runtime
node --trace-deopt server.js 2>&1 | grep -i 'deopt'
# Em flamegraph (clinic Flame, 0x): blocos vermelhos = deopted; amarelo = optimized eager; verde = not yet optimized.
# Hot path com vermelho = perda significativa.
Pattern Logística — fix polymorphic deopt no preço:
// BAD — shapes variando entre chamadas
function calculatePrice(item: any) {
return item.price * item.quantity; // V8 deopta quando shapes divergem
}
calculatePrice({ price: 10, quantity: 2 }); // {price, quantity}
calculatePrice({ price: 5, quantity: 1, tax: 0 }); // shape diferente
calculatePrice({ price: 8, quantity: 3, discount: 1 }); // shape diferente de novo
// GOOD — monomorphic, shape fixo
interface PricedItem { price: number; quantity: number; tax: number; discount: number }
function calculatePrice(item: PricedItem) { // sempre mesmo shape; TurboFan inlina
return item.price * item.quantity * (1 + item.tax) - item.discount;
}
Heap snapshots — leak detection real:
// Snapshot programático
import v8 from 'node:v8';
v8.writeHeapSnapshot('./snapshot-' + Date.now() + '.heapsnapshot');
# Auto-snapshot perto de OOM (Node 12+): captura 3 snapshots antes de crash
node --heapsnapshot-near-heap-limit=3 server.js
# Análise: Chrome DevTools → Memory → Load profile
Pyroscope — continuous profiling em produção:
import Pyroscope from '@pyroscope/nodejs'; // SDK 2026, version 0.21+
Pyroscope.init({
serverAddress: process.env.PYROSCOPE_URL!,
appName: 'orders-api',
tags: {
region: process.env.REGION!,
version: process.env.VERSION!, // CRÍTICO pra diff entre deploys
},
});
Pyroscope.start();
region, version, env). NUNCA user_id, tenant_id, request_id — explosion de séries.Profiling em produção — sem matar latency:
--cpu-prof): cada call gravado; alto overhead; só windows curtos (segundos).Logística — investigation real (deploy v3.4 → v3.5):
/orders p99 latency 100ms → 800ms após deploy v3.4. Sem alert óbvio em CPU/memory.JSON.parse consumindo 60% CPU em hot path; payload de 12KB+ por request.JSON.parse < 5%.Anti-patterns observados:
--trace-deopt deveria ser 0 linhas em código quente. Cada deopt = JIT desistiu.try/catch em hot inner loop pre-Node 14: deopta. Refatore catch pro outer scope.--inspect permanente em prod: porta de debugger exposta + overhead. Usar só ad-hoc com firewall.tenant_id, user_id): explosion de séries no backend storage.Cruza com 02-07 §2.18 (event loop blocking + worker_threads — profiling localiza onde bloquear), 03-09 (frontend perf, mesmos princípios de flamegraph), 03-07 (observability stack, profiling como pilar adjacente a logs/metrics/traces), 03-10 (backend perf patterns broader), 04-09 (scaling, capacity planning profile-driven).
Node passou por renaissance 2024-2026. Bun 1.2 (Q1 2026) e Deno 2.x (Q4 2024) pressionaram o ecossistema com batteries-included: test runner nativo, TS sem build, SQLite embedded, WebSocket client, watch mode. Node 22 LTS (Apr 2024) e Node 24 LTS (Apr 2025) responderam absorvendo essas features no core — Permission Model GA, node:test mature, --experimental-strip-types, node:sqlite, native WebSocket stable, --watch + --env-file. Resultado: razões pra adotar runtime alternativo encolheram. Bun ainda vence em startup + bundle (cold start ~3x mais rápido), Deno em security-by-default + URL imports. Node vence em compat + ecossistema + LTS previsível. Node 25 Current (Q4 2025) traz V8 13.x com Maglev mais maduro (já em Node 22 com V8 12.4, ~5-15% throughput em hot paths sintéticos).
Sandbox granular no runtime — sem container/SELinux. Defesa-em-profundidade contra supply-chain attack (lib maliciosa em node_modules tentando ler ~/.aws/credentials ou abrir socket pra C2).
# Sem permission model: lib pode ler tudo, abrir socket pra qualquer host
node app.js
# Com permission model (Node 24 GA — sem --experimental)
node --permission \
--allow-fs-read=./data,./config \
--allow-fs-write=./logs,./tmp \
--allow-net=api.stripe.com:443,postgres-internal:5432 \
--allow-child-process \
--allow-worker \
app.js
API runtime pra checar:
import { permission } from 'node:process';
if (!permission.has('fs.read', '/etc/passwd')) {
throw new Error('FS read denied — esperado');
}
Custo: ~5-10% overhead em workload syscall-heavy (FS scan, muitas conexões). Worker threads herdam permissions do parent. Node addons nativos (.node) bloqueados por default — --allow-addons libera (cuidado: addon bypassa o modelo).
Production hardening: rode workers Cloud Run / Kubernetes com --permission --allow-fs-read=/app --allow-net=postgres-internal:5432,redis-internal:6379. Postmortem-friendly: tentativa de ler /etc/shadow lança ERR_ACCESS_DENIED com stack trace, não silently succeeds.
Zero-config, pure ESM, sem dep externa. API estilo Mocha/Jest familiar.
// test/user.test.js
import { describe, it, before, after, mock } from 'node:test';
import assert from 'node:assert/strict';
import { createUser } from '../src/user.js';
describe('createUser', () => {
let db;
before(async () => { db = await openTestDb(); });
after(async () => { await db.close(); });
it('creates user with hashed password', async () => {
const user = await createUser(db, { email: 'a@b.com', password: 'p' });
assert.equal(user.email, 'a@b.com');
assert.notEqual(user.password, 'p'); // hashed
});
it('mocks external API', async (t) => {
const fetchMock = t.mock.method(global, 'fetch', async () => ({
ok: true, json: async () => ({ valid: true })
}));
await createUser(db, { email: 'x@y.com', password: 'p' });
assert.equal(fetchMock.mock.callCount(), 1);
});
});
node --test --test-reporter=spec test/
node --test --experimental-test-coverage # built-in coverage Node 22+
node --test --watch # re-roda em mudança
Quando usar node:test vs vitest: node:test vence em projeto pure ESM monorepo backend sem Vite (zero-config, ~2x faster cold start em suites pequenas <50 testes, sem vitest.config.ts pra manter). Vitest vence em frontend / fullstack com Vite (HMR test, browser mode, jsdom integrado, snapshot mais maduro, plugin ecosystem). Não migre vitest funcional pra node:test só por modismo — custo de migração > benefício.
Roda .ts direto, sem tsc / tsx / ts-node. Apenas remove tipos — não transpila enum, namespace, parameter properties, decorators legacy.
node --experimental-strip-types src/server.ts
# Com transform (suporta enum, namespace — Node 22.7+)
node --experimental-transform-types src/server.ts
Limitação crítica: enum Color { Red, Green } em --strip-types puro = silent miscompile (vira nada, runtime error em uso). Use --experimental-transform-types ou prefira union literal type Color = 'red' | 'green'. Declaration merging, parameter properties (constructor(private x: number)), import = require() também não suportados em strip puro.
Quando usar: scripts internos, CLIs, dev/test loop rápido. Não use em prod com bundle complexo — esbuild/swc são mais rápidos em suites grandes e suportam tudo. Útil pra eliminar tsx em package.json scripts simples.
SQLite embedded no core. Sem build native (better-sqlite3 requer Python + node-gyp em CI). Synchronous API.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('cache.db');
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_expires ON sessions(expires_at);
`);
const insert = db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)');
insert.run('sess_abc', 42, Date.now() + 3600_000);
const get = db.prepare('SELECT * FROM sessions WHERE id = ? AND expires_at > ?');
const session = get.get('sess_abc', Date.now());
Use em: cache local de CLI tool, embedded config DB, dev fixtures, test isolation. Não use em: banco compartilhado entre processos (single-process write lock — use Postgres), workload write-heavy concorrente (better-sqlite3 ainda ~10-20% mais rápido em micro-benchmarks).
API Web standard — mesma do browser. Elimina ws lib pra cliente.
const ws = new WebSocket('wss://stream.binance.com:9443/ws/btcusdt@trade');
ws.addEventListener('open', () => console.log('connected'));
ws.addEventListener('message', (ev) => {
const trade = JSON.parse(ev.data);
console.log(trade.p, trade.q);
});
ws.addEventListener('close', (ev) => console.log('closed', ev.code));
ws.addEventListener('error', (err) => console.error(err));
Limitação: sem auto-reconnect, sem heartbeat/ping built-in, sem subprotocol negotiation avançada. Pra prod com reconexão exponential backoff + ping/pong, ainda precisa wrapper (ou ws lib pra server-side). Use built-in pra clients simples + scripts.
node --watch --env-file=.env src/server.js
node --watch-path=./src --watch-path=./config src/server.js
--watch substitui nodemon. --env-file=.env substitui dotenv (parser simples — KEY=value, sem expansion ${OTHER_VAR} em .env por default; Node 22+ aceita --env-file-if-exists). Não use --watch em prod — filewatcher overhead + restart loop em log rotation.
Bundle JS + Node runtime em 1 binário. Distribui CLI sem requerir Node instalado.
// sea-config.json
{
"main": "dist/cli.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true
}
node --experimental-sea-config sea-config.json
cp $(command -v node) my-cli
npx postject my-cli NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
./my-cli # standalone binary ~80MB
Limitação: addons nativos (.node) limitados (top-level require de .node não funciona em SEA stable). Use pra CLI puro JS. Alternativa: pkg (deprecated), bun build --compile (Bun ~6MB binary, vence Node ~80MB).
Workspaces protocol ("dep": "workspace:*"), install perf melhorada, npm audit signatures por default. Pin no package.json:
{
"engines": { "node": ">=22.0.0", "npm": ">=11.0.0" },
"packageManager": "npm@11.0.0"
}
CI sem pin: usa npm bundled com Node version do runner — pode ser npm 8/9 e quebrar workspaces protocol.
node --permission --allow-fs-read=/app --allow-net=postgres-internal:5432,redis-internal:6379 dist/worker.js — supply-chain defense.node:test (backend pure ESM monorepo) — eliminou vitest config, CI ~30% mais rápido em cold start. E2E continua Playwright.node:sqlite em CLI interna de batch reprocess — embedded cache de mapeamentos, sem subir Postgres local.--watch --env-file=.env em dev — eliminou nodemon + dotenv.ws dep.--permission sem --allow-fs-* em workload com FS legítimo — ERR_ACCESS_DENIED em runtime, postmortem barulhento.--experimental-strip-types em código com enum — silent miscompile, NaN/undefined em runtime. Use --experimental-transform-types ou union literal.node:test em projeto frontend — perde browser mode, jsdom, HMR test. Custo > benefício.node:sqlite pra banco compartilhado entre múltiplos processos — single-writer lock, contention. Use Postgres.--watch em prod (Docker, systemd) — filewatcher overhead + restart loop em log rotation, port-already-in-use.sharp, better-sqlite3, bcrypt) — .node addons não funcionam stable em SEA.package.json sem engines.npm em projeto com workspaces protocol — CI usa npm bundled (8/9), workspace:* quebra.node --permission --allow-net=* (wildcard amplo) — anula o modelo, vira teatro de segurança.02-07 §2.7 (streams sob fetch nova — Web Streams API mainstreamed Node 22+), §2.9 (worker_threads herdam permission model), §2.14 (AsyncLocalStorage relevante a permission scoping por request), §2.17 (Node vs Bun vs Deno — Node 24 fechou maioria dos gaps), §2.18 (worker_threads + cluster), 03-01 (testing — node:test categoria backend pure), 03-08 (security — Permission Model é defense-in-depth contra supply-chain), 03-04 (CI/CD — engines.npm + Node version pinning em workflow), 03-02 (Docker — distroless + SEA como alternativa de distribuição).
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.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Em que ordem o Node.js drena callbacks após o código síncrono terminar?
Q2Quais APIs do Node usam o thread pool da libuv (default 4 threads)?
Q3Quando você deve usar `worker_threads` em vez de `cluster`?
Q4Por que `cluster` é geralmente desnecessário (e até prejudicial) em deploy Kubernetes?
Q5O que o Permission Model GA do Node 24 entrega como defesa-em-profundidade?
Destrava
02-07 é prereq dos seguintes módulos: