Al Mòdul 1 vas fer servir Node.js com una caixa negra: vas escriure node src/cataleg.js i van aparèixer esdeveniments per pantalla. Va funcionar, però no saps què hi ha dins d'aquesta caixa. I aquest desconeixement té conseqüències molt concretes: no sabries explicar per què un servidor Node aguanta deu mil connexions simultànies amb un sol procés, per què llegir un fitxer gran no bloqueja ningú però calcular un hash sí, ni per què el consell «no bloquegis el fil principal» es repeteix fins a l'avorriment en tota la documentació.
Aquesta lliçó obre la caixa. Veurem les capes reals que componen Node.js —el teu JavaScript, la biblioteca estàndard, els bindings, V8 i libuv—, què fa exactament cadascuna, i el matís que gairebé tothom repeteix malament: Node.js no és d'un sol fil del tot. Hi ha un fil principal on viu el teu codi i un grup de fils auxiliars on passen coses que tu no veus mai. En acabar sabràs quines operacions van a cada lloc, ho podràs mesurar amb experiments executables, i entendràs per què bloquejar el fil principal és el pecat capital del desenvolupament en Node.
És la base sobre la qual s'aguanta tota la resta del mòdul. Sense ella, el bucle d'esdeveniments de la lliçó següent és màgia; amb ella, és mecànica.
Contingut
- La pila de Node.js capa per capa
- V8: el motor que executa el teu JavaScript
- libuv: el motor que fa possible l'asincronia
- El matís clau: Node no és d'un sol fil del tot
- Què va al thread pool i què no
- Demostració mesurable: els quatre fils i el cinquè que espera
- El pecat capital: bloquejar el fil principal
- El cicle de vida d'un procés Node
- Inspeccionar el procés:
process.versioniprocess.memoryUsage()
- La pila de Node.js capa per capa
Quan executes node src/cataleg.js, aquesta ordre posa en marxa un programa escrit en C++ que conté, empaquetats dins del mateix binari, un motor de JavaScript i una biblioteca d'entrada/sortida asíncrona. El teu fitxer .js és, simplement, la dada d'entrada d'aquest programa.
Aquestes són les capes, de dalt a baix:
flowchart TD
A["<b>El teu codi JavaScript</b><br/>src/cataleg.js, src/domini/esdeveniment.js"]
B["<b>Biblioteca estandard de Node</b> (JavaScript)<br/>fs, http, path, events, crypto...<br/>lib/*.js dins del binari"]
C["<b>Bindings</b> (C++)<br/>El pont entre JavaScript i C++<br/>node_file.cc, node_http_parser.cc..."]
D1["<b>V8</b> (C++)<br/>Executa JavaScript:<br/>compila, gestiona memoria"]
D2["<b>libuv</b> (C)<br/>Bucle d'esdeveniments, thread pool,<br/>E/S asincrona del sistema"]
D3["<b>Altres biblioteques</b> (C/C++)<br/>OpenSSL, zlib, c-ares, llhttp"]
E["<b>Sistema operatiu</b><br/>epoll (Linux) · kqueue (macOS) · IOCP (Windows)"]
A --> B
B --> C
C --> D1
C --> D2
C --> D3
D1 --> E
D2 --> E
D3 --> E
Anem capa per capa amb un exemple concret: la crida fs.readFile('dades/esdeveniments.json', callback) que escriuràs de debò al Mòdul 3.
| Capa | Què és | Què fa amb fs.readFile |
|---|---|---|
| El teu codi | Els .js que escrius |
Crida fs.readFile amb una ruta i un callback |
| Biblioteca estàndard | Mòduls de Node escrits en JavaScript, precompilats dins del binari | Valida els arguments, normalitza la ruta, prepara l'objecte de petició i crida el binding |
| Bindings | Codi C++ que exposa funcions natives a JavaScript | Tradueix els valors de JavaScript a tipus de C++ i crida uv_fs_read |
| libuv | Biblioteca C d'E/S asíncrona | Encua la lectura al seu thread pool i, quan acaba, avisa |
| V8 | Motor de JavaScript | Executa el teu callback quan Node l'hi demana |
| Sistema operatiu | El nucli | Fa la lectura real del disc |
Dues idees importants d'aquesta taula:
- Bona part de Node.js està escrita en JavaScript, no pas en C++. Els mòduls
fs,http,path,events,stream… són fitxers.jsincrustats dins del binari. Els pots llegir: són al repositori de Node, a la carpetalib/. Això vol dir que la biblioteca estàndard de Node és, en gran mesura, codi com el teu. - Els bindings són la frontera. Tot allò que JavaScript no pot fer per si sol —obrir un socket, llegir un fitxer, xifrar— travessa aquesta frontera cap a C++. Travessar-la té un cost petit però no nul, i per això convé fer poques crides grans en comptes de moltes de petites.
Comprovació ràpida. Node exposa quines versions de cada dependència porta a dins. Executa això al teu terminal:
node -p "JSON.stringify(process.versions, null, 2)"Veuràs una cosa semblant a
{ "node": "22.11.0", "v8": "12.4.254.21", "uv": "1.48.0", "openssl": "3.0.15", "zlib": "1.3.1", ... }. Cadascuna d'aquestes línies és una de les biblioteques del diagrama.
- V8: el motor que executa el teu JavaScript
V8 és el motor de JavaScript de Google, el mateix que fa servir Chrome. La seva feina és una de sola: convertir el teu codi JavaScript en instruccions de màquina i executar-les. V8 no sap res de fitxers, ni de xarxa, ni de temporitzadors. Això no forma part de JavaScript; ho posa Node.
V8 fa tres coses que t'afecten directament.
2.1 Compilació JIT (just-in-time)
JavaScript no s'interpreta línia a línia com als anys noranta. V8 fa servir una canonada de diverses etapes:
- Anàlisi i bytecode. Un intèrpret anomenat Ignition converteix el teu codi en un bytecode compacte i l'executa. És ràpid d'arrencar però moderat en velocitat.
- Observació. Mentre s'executa, V8 registra quines funcions es criden molt i amb quins tipus de dades.
- Optimització. Un compilador optimitzador (TurboFan) agafa les funcions «calentes» i genera codi màquina especialitzat per als tipus que ha observat.
- Desoptimització. Si de sobte crides aquesta funció amb un tipus diferent del previst, V8 descarta el codi optimitzat i torna al bytecode.
La conseqüència pràctica per a tu: la coherència de tipus importa. Una funció que de vegades rep un número i de vegades una cadena és més lenta que una que sempre rep un número.
// src/laboratori/tipus.js
// Be: preuCentims es SEMPRE un enter.
function calcularTotalCentims(sessions) {
return sessions.reduce((total, s) => total + s.venudes * s.preuCentims, 0);
}
// Malament: si preuCentims de vegades arriba com a cadena ('2500'), V8 no pot
// especialitzar la funcio i, a mes, el '+' concatena en lloc de sumar.Aquesta és una altra raó —a banda de l'exactitud dels diners— per a la nostra convenció de desar els imports sempre com a enters de cèntims: cada camp té un únic tipus durant tota la seva vida.
2.2 El munt (heap) i la memòria
V8 gestiona la memòria de tots els teus objectes en una zona anomenada munt, dividida en dues regions:
| Regió | Què conté | Com es neteja |
|---|---|---|
| Nova generació (new space) | Objectes acabats de crear. És petita (uns pocs MB) | Recol·lecció molt freqüent i molt ràpida (scavenge) |
| Vella generació (old space) | Objectes que han sobreviscut a diverses recol·leccions | Recol·lecció menys freqüent i més costosa (mark-sweep-compact) |
La hipòtesi en què es basa aquest disseny és que la majoria dels objectes moren joves. A Escena Viva, l'objecte temporal que crees per formatar una data mor en microsegons; l'array cataleg viu tot el procés. Separar-los permet netejar allò efímer molt barat.
2.3 Recol·lecció d'escombraries
En JavaScript no alliberes memòria a mà: V8 detecta quins objectes ja no són accessibles des de cap variable viva i en recupera l'espai. El que has de saber:
- La recol·lecció d'escombraries pausa l'execució del teu JavaScript. Les pauses de la nova generació són de microsegons i no es noten; les de la vella generació poden ser de mil·lisegons i sí que es noten en un servidor carregat.
- El munt té un límit. Per defecte ronda els 4 GB en versions modernes de 64 bits, i s'ajusta amb
node --max-old-space-size=2048. Si el superes, el procés mor ambJavaScript heap out of memory. - Es poden tenir fuites de memòria en JavaScript. No pas per oblidar-te d'alliberar, sinó per continuar referint-te a coses que ja no necessites: una memòria cau que creix sense límit, un array que només acumula, o —cas molt típic i que veurem a la lliçó Esdeveniments i EventEmitter— oients que es registren i no es desregistren mai.
- libuv: el motor que fa possible l'asincronia
Si V8 aporta l'«executar JavaScript», libuv aporta el «fer-ho sense esperar». És una biblioteca escrita en C, nascuda precisament per a Node.js, l'objectiu de la qual és oferir una única API asíncrona multiplataforma per damunt de mecanismes de sistema operatiu que són completament diferents entre si.
libuv té tres responsabilitats:
- El bucle d'esdeveniments (event loop). El cicle infinit que pregunta «hi ha res acabat? hi ha cap callback per executar?» i va processant la feina pendent. És tan central que li dediquem sencera la lliçó següent, El Bucle d'Esdeveniments (Event Loop).
- L'E/S asíncrona del sistema operatiu. Per a la xarxa, libuv fa servir el mecanisme natiu de notificació de cada plataforma:
epolla Linux,kqueuea macOS i BSD, event ports a Solaris i IOCP a Windows. Amb aquests mecanismes, un sol fil pot vigilar milers de sockets alhora sense consumir un fil per connexió. - La cua de treball i el thread pool. Per a les operacions que el sistema operatiu no ofereix de manera asíncrona fiable, libuv manté un grup de fils reals als quals envia la feina. És la part que genera més malentesos i la que veurem en detall tot seguit.
flowchart LR
JS["El teu JavaScript<br/>(fil principal)"] -->|"peticio asincrona"| UV["libuv"]
UV -->|"xarxa, sockets, canonades"| OS["epoll / kqueue / IOCP<br/>(sense fils extra)"]
UV -->|"fitxers, DNS, crypto, zlib"| TP["Thread pool<br/>4 fils per defecte"]
OS -->|"a punt"| CB["Cua de callbacks"]
TP -->|"a punt"| CB
CB -->|"el bucle els executa"| JS
Llegeix el diagrama en veu alta un cop: el teu codi demana una cosa, libuv decideix per quin camí resoldre-la, la feina es fa fora del teu fil i, quan acaba, el resultat torna al teu fil en forma de callback. Aquest viatge d'anada i tornada és tota l'asincronia de Node.
- El matís clau: Node no és d'un sol fil del tot
La frase «Node.js és d'un sol fil» es repeteix arreu i és mitja veritat. La versió precisa és:
El teu codi JavaScript s'executa en un únic fil. La feina d'entrada/sortida, no.
Si obres el monitor de processos mentre corre un programa Node, veuràs que el procés té diversos fils: el principal, els quatre del thread pool, i alguns més de V8 per a la recol·lecció d'escombraries i la compilació en segon pla.
Comprova-ho tu mateix:
# Arrenca un proces Node que no acabi
node -e "setInterval(() => {}, 1000)" &
# Linux: comptar els fils del proces
ps -o nlwp -p $!
# NLWP
# 11 <- onze fils, no pas unA macOS pots fer servir ps -M <pid>; a Windows, l'Administrador de tasques amb la columna «Subprocessos» activada.
El que això significa a la pràctica:
| Afirmació | És certa? | Matís |
|---|---|---|
| «Node executa el meu JavaScript en un sol fil» | Sí | Dues funcions teves no corren mai alhora. Per això no hi ha condicions de carrera sobre les variables |
| «Node és monofil, punt» | No | El procés té un thread pool i fils interns de V8 |
| «Node pot aprofitar els 8 nuclis de la meva màquina» | Parcialment | L'E/S sí; el teu JavaScript no. Per a això hi ha cluster i worker_threads del Mòdul 10 |
| «Si bloquejo el fil principal, només s'alenteix aquella petició» | No | Es bloquegen totes les peticions d'aquell procés |
L'avantatge que el teu JavaScript sigui monofil és enorme i sovint s'oblida: no existeixen condicions de carrera dins d'un bloc de codi síncron. Quan a Escena Viva escriguis sessio.venudes += quantitat, ningú no es pot colar entre la lectura i l'escriptura. En Java o en Go hauries de pensar en un pany; aquí, no. El preu d'aquesta tranquil·litat és que tu ets responsable de tornar el control de pressa.
- Què va al thread pool i què no
Aquesta és probablement la taula més útil de la lliçó. El thread pool de libuv té 4 fils per defecte i es configura amb la variable d'entorn UV_THREADPOOL_SIZE (màxim 1024 en versions modernes).
| Operació | Camí | Per què |
|---|---|---|
| Sockets TCP/UDP, servidor i client HTTP | epoll/kqueue/IOCP | El sistema operatiu ja ofereix notificació asíncrona nativa per a la xarxa |
| Canonades (pipes) i processos fills | epoll/kqueue/IOCP | Igual que la xarxa |
fs.readFile, fs.writeFile, fs.stat… |
Thread pool | L'E/S de fitxers asíncrona del sistema és inconsistent entre plataformes; libuv la simula amb fils |
crypto.pbkdf2, crypto.scrypt, crypto.randomBytes (versió asíncrona) |
Thread pool | Són càlcul pur de CPU; al fil principal el bloquejarien |
zlib.gzip, zlib.deflate (versions asíncrones) |
Thread pool | Compressió: també càlcul intensiu |
dns.lookup |
Thread pool | Fa servir getaddrinfo del sistema, que és una crida bloquejant |
dns.resolve, dns.resolve4… |
epoll/kqueue/IOCP | Fa servir la biblioteca c-ares, que parla DNS per xarxa de manera asíncrona |
setTimeout, setInterval, setImmediate |
Ni l'un ni l'altre | Els gestiona el mateix bucle d'esdeveniments, sense fils ni E/S |
El teu bucle for sobre 3000 sessions |
Fil principal | És JavaScript. Ningú no el mourà a cap altre lloc |
Fixa't en el detall del DNS: dns.lookup i dns.resolve no són sinònims. dns.lookup és el que fa servir per sota http.request quan li dones un nom de host, i consumeix un fil del thread pool. És un clàssic: una aplicació que fa moltes peticions HTTP sortints exhaureix els quatre fils amb resolucions de DNS i, de sobte, les lectures de fitxer es tornen lentes sense motiu aparent.
I fixa't en l'última fila, perquè és la que resumeix la lliçó: per al càlcul en JavaScript no hi ha sortida. El thread pool només serveix el codi natiu de Node. Si tu escrius un bucle que triga dos segons, aquests dos segons els paga el fil principal.
- Demostració mesurable: els quatre fils i el cinquè que espera
Prou teoria. Anem a mesurar el thread pool. Farem servir crypto.pbkdf2, una funció de derivació de claus (la mateixa família que farem servir al Mòdul 8 per a les contrasenyes) que consumeix CPU de manera intensiva i que libuv envia al thread pool.
Crea src/laboratori/thread-pool.js:
// src/laboratori/thread-pool.js
// Demostra que el thread pool de libuv te 4 fils per defecte.
// Llancem N tasques identiques de CPU i mesurem quan acaba cadascuna.
const crypto = require('node:crypto');
// Quantes tasques llancar: per defecte 5, o el primer argument de la linia d'ordres.
const TASQUES = Number(process.argv[2]) || 5;
// Parametres de pbkdf2: contrasenya, sal, iteracions, longitud, algorisme.
// 200000 iteracions triguen de l'ordre de 100-200 ms en una maquina normal.
const ITERACIONS = 200000;
const inici = Date.now();
console.log(`Thread pool: ${process.env.UV_THREADPOOL_SIZE || 4} fils`);
console.log(`Llancant ${TASQUES} tasques de CPU...`);
console.log('');
for (let i = 1; i <= TASQUES; i++) {
crypto.pbkdf2('entrada-escena-viva', 'sal', ITERACIONS, 64, 'sha512', () => {
const transcorregut = Date.now() - inici;
console.log(`Tasca ${i} acabada en ${transcorregut} ms`);
});
}Executa:
Sortida típica (els mil·lisegons variaran segons la teva màquina, però el patró serà el mateix):
Thread pool: 4 fils Llancant 5 tasques de CPU... Tasca 1 acabada en 168 ms Tasca 2 acabada en 171 ms Tasca 3 acabada en 172 ms Tasca 4 acabada en 175 ms Tasca 5 acabada en 338 ms <-- el doble
Llegeix bé aquest resultat, perquè conté tota la lliçó:
- Les quatre primeres tasques acaben gairebé alhora, al voltant dels 170 ms. S'han executat en paral·lel de debò, cadascuna en un fil del thread pool, en quatre nuclis diferents del teu processador.
- La cinquena triga aproximadament el doble. No hi havia un cinquè fil lliure: ha hagut d'esperar en cua que un dels quatre s'alliberés.
Ara puja la mida del thread pool i torna a executar-lo:
# Linux i macOS
UV_THREADPOOL_SIZE=5 node src/laboratori/thread-pool.js 5
# Windows (PowerShell)
# $env:UV_THREADPOOL_SIZE=5; node src/laboratori/thread-pool.js 5Thread pool: 5 fils Llancant 5 tasques de CPU... Tasca 1 acabada en 178 ms Tasca 2 acabada en 181 ms Tasca 3 acabada en 182 ms Tasca 4 acabada en 184 ms Tasca 5 acabada en 185 ms <-- ara tambe en paral·lel
Les cinc acaben alhora. Acabes de comprovar experimentalment la mida del thread pool i el seu efecte.
Compte amb la temptació de pujar-lo molt.
UV_THREADPOOL_SIZE=128no multiplica el rendiment per 32: si la teva màquina té 8 nuclis, 128 fils competeixen pels mateixos 8 nuclis i afegeixen cost de canvi de context. La regla raonable és acostar-lo al nombre de nuclis quan la teva aplicació fa molta E/S de fitxers o moltcrypto, i mesurar sempre abans i després. La variable ha d'estar posada abans d'arrencar el procés: canviar-la des de dins ambprocess.env.UV_THREADPOOL_SIZE = 8no té cap efecte, perquè el pool ja s'ha creat.
- El pecat capital: bloquejar el fil principal
Ara l'experiment complementari, i el més important de la lliçó. Què passa si la feina pesada la fas en JavaScript en comptes de delegar-la?
Crea src/laboratori/bloqueig.js:
// src/laboratori/bloqueig.js
// Demostra que un bucle sincron congela TOT el proces.
const inici = Date.now();
// Un temporitzador que hauria de disparar-se cada 100 ms.
// El fem servir com a "electrocardiograma" del proces.
const batec = setInterval(() => {
console.log(` batec als ${Date.now() - inici} ms`);
}, 100);
// Aturem l'experiment als 2 segons de rellotge.
setTimeout(() => {
clearInterval(batec);
console.log('Fi.');
}, 2000);
// Tasca sincrona pesada: sumar 3.000 milions de vegades.
// Simula un calcul mal plantejat sobre el cataleg.
function bloquejarDurant(ms) {
const limit = Date.now() + ms;
while (Date.now() < limit) {
// Bucle buit a proposit: representa qualsevol calcul llarg.
}
}
// Als 300 ms, bloquegem el fil principal durant un segon sencer.
setTimeout(() => {
console.log('>> Comenca el bloqueig de 1000 ms');
bloquejarDurant(1000);
console.log('>> Acaba el bloqueig');
}, 300);Executa node src/laboratori/bloqueig.js:
batec als 103 ms batec als 205 ms >> Comenca el bloqueig de 1000 ms >> Acaba el bloqueig batec als 1306 ms <-- el batec dels 400, 500, 600... no va arribar mai batec als 1406 ms batec als 1507 ms ... Fi.
Mira el forat: entre els 205 ms i els 1306 ms no hi va haver ni un sol batec. Durant un segon sencer el procés va estar viu però completament sord. Els temporitzadors no es van disparar i, si hagués estat un servidor HTTP, cap petició no s'hauria atès: s'haurien quedat a la cua del sistema operatiu esperant que el fil principal tornés a estar lliure.
Traduït a Escena Viva: si un usuari demana un informe que triga un segon a calcular-se de manera síncrona, els altres tres-cents usuaris que estan comprant entrades en aquell moment esperen també aquest segon. No és que la seva petició sigui més lenta; és que el servidor sencer s'ha aturat.
Aquesta és la diferència fonamental amb el model d'un fil per petició que vas veure a la primera lliçó: allà, una petició lenta perjudica només qui l'ha feta. Aquí perjudica tothom. És el preu de l'eficiència del model de Node, i explica la regla que governarà la resta del curs:
Tot el que facis al fil principal ha de durar poc. El que duri molt, delega-ho.
Les maneres de delegar, per ordre de preferència:
| Situació | Solució | On es veu |
|---|---|---|
| E/S: fitxers, xarxa, base de dades | Fer servir la versió asíncrona de l'API, mai la ...Sync |
Mòduls 3, 4 i 7 |
| Càlcul pesat que Node ja ofereix natiu | crypto, zlib en la seva versió asíncrona (van al thread pool) |
Aquest mateix mòdul |
| Càlcul pesat en JavaScript propi | worker_threads: un fil a part amb el seu propi V8 |
Mòdul 10 |
| Aprofitar tots els nuclis per atendre trànsit | cluster: diversos processos Node repartint-se el port |
Mòdul 10 |
| Feina que pot esperar (correus, PDF) | Treure-la del procés: una cua de treballs | Mòdul 10 |
És exactament la promesa que es va fer a la lliçó Què és Node.js? quan vam dir que el càlcul intensiu de CPU era el punt feble de Node i que el Mòdul 10 ho resoldria. Ara ja saps per què és el punt feble.
- El cicle de vida d'un procés Node
Queda una pregunta que potser no t'havies fet: quan vas executar node src/cataleg.js, el programa va imprimir el catàleg i va acabar sol. Però si executes node -e "setInterval(() => {}, 1000)", el procés no acaba mai. Qui ho decideix?
El cicle de vida d'un procés Node té aquestes etapes:
flowchart TD
A["1. Arrencada<br/>El binari inicialitza V8 i libuv"] --> B["2. Bootstrap<br/>Es prepara process, console,<br/>i els moduls interns"]
B --> C["3. Execucio de l'script<br/>S'executa el teu fitxer de dalt a baix,<br/>de manera sincrona i completa"]
C --> D{"4. Queden tasques<br/>pendents?"}
D -->|"Si"| E["5. Bucle d'esdeveniments<br/>Espera, executa callbacks,<br/>torna a preguntar"]
E --> D
D -->|"No"| F["6. Sortida<br/>S'emet 'exit' i el proces<br/>retorna process.exitCode"]
La clau és al rombe del pas 4. libuv porta un compte de referències de les tasques actives: temporitzadors programats, sockets oberts, operacions de fitxer en curs, servidors escoltant. Mentre aquest compte sigui més gran que zero, el bucle continua girant. Quan arriba a zero, no queda res que pugui passar mai, així que Node tanca ordenadament i torna el control al terminal.
Això explica els dos casos:
// src/laboratori/cicle-vida.js
console.log("1. Comenca l'script");
// Aixo AFEGEIX una referencia: hi ha una tasca futura pendent.
setTimeout(() => {
console.log('3. Temporitzador de 500 ms disparat');
// En executar-se, la referencia s'allibera. Ja no queda res -> sortida.
}, 500);
console.log("2. Acaba l'script (pero el proces continua viu)");
// L'esdeveniment 'exit' s'emet just abans de morir.
process.on('exit', (codi) => {
// Aqui nomes es pot executar codi SINCRON: el bucle ja esta tancat.
console.log(`4. Sortint amb codi ${codi}`);
});node src/laboratori/cicle-vida.js
# 1. Comenca l'script
# 2. Acaba l'script (pero el proces continua viu)
# 3. Temporitzador de 500 ms disparat
# 4. Sortint amb codi 0Un setInterval, en canvi, no allibera mai la seva referència perquè sempre hi ha una propera execució programada: el procés viu per sempre fins que algú cridi clearInterval o el matis amb Ctrl+C. El mateix passa amb un servidor HTTP escoltant, i és justament el que volem: un servidor que es tanqués sol no serviria de gaire.
Hi ha una via d'escapament, unref(), que marca una tasca com a «no compta per mantenir viu el procés»:
// Aquest temporitzador NO impedeix que el proces acabi.
const vigilant = setInterval(() => {
console.error(`Memoria: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)} MB`);
}, 1000);
vigilant.unref();És el patró habitual de les tasques de vigilància i mètriques: vols que informin mentre l'aplicació visqui, però no que la mantinguin viva.
I per acabar, la sortida forçada:
| Forma | Efecte |
|---|---|
| S'acaben les tasques pendents | Sortida natural i neta. El normal |
process.exitCode = 1 |
Marca el codi de sortida i deixa acabar amb normalitat. La manera correcta d'indicar un error |
process.exit(1) |
Talla immediatament: les escriptures a mitges es perden, els callbacks pendents no s'executen. Fer-ho servir només en casos extrems |
| Excepció no capturada | El procés mor amb codi 1 i traça a stderr |
Ja vas fer servir process.exitCode a la lliçó El Teu Primer Programa en Node.js; ara ja saps per què es prefereix a process.exit().
- Inspeccionar el procés:
process.version i process.memoryUsage()
process.version i process.memoryUsage()Node et deixa mirar dins seu en temps d'execució. Dues eines que faràs servir constantment.
9.1 Versions
// src/laboratori/diagnostic.js
console.log(`Node.js : ${process.version}`); // v22.11.0
console.log(`V8 : ${process.versions.v8}`); // 12.4.254.21
console.log(`libuv : ${process.versions.uv}`); // 1.48.0
console.log(`OpenSSL : ${process.versions.openssl}`);
console.log(`Plataforma: ${process.platform} ${process.arch}`); // linux x64
console.log(`Nuclis : ${require('node:os').cpus().length}`);
console.log(`PID : ${process.pid}`);process.version (singular, amb v al davant) és la versió de Node; process.versions (plural) és l'objecte amb totes les dependències. Es confonen constantment.
Un ús real: comprovar en arrencar que es compleix la versió mínima que el projecte necessita.
// src/utils/comprovar-versio.js
// Avorta si la versio de Node es anterior a la minima suportada.
const MAJOR_MINIM = 20;
// process.version es 'v22.11.0'; treiem la 'v' i ens quedem amb el primer numero.
const majorActual = Number(process.version.slice(1).split('.')[0]);
if (majorActual < MAJOR_MINIM) {
console.error(
`Escena Viva necessita Node.js ${MAJOR_MINIM} o superior. ` +
`Tens ${process.version}. Executa "nvm use" a l'arrel del projecte.`
);
process.exitCode = 1;
} else {
console.log(`Versio de Node correcta: ${process.version}`);
}Recorda la convenció del projecte: el diagnòstic va per stderr (console.error), les dades per stdout.
9.2 Memòria
process.memoryUsage() retorna un objecte amb el consum actual, en bytes:
// src/laboratori/memoria.js
// Mesura quanta memoria ocupa carregar un cataleg gran en memoria.
function enMegues(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function informarMemoria(etiqueta) {
const m = process.memoryUsage();
console.log(
`${etiqueta.padEnd(22)} rss=${enMegues(m.rss).padStart(9)} ` +
`heapTotal=${enMegues(m.heapTotal).padStart(9)} ` +
`heapUsed=${enMegues(m.heapUsed).padStart(9)}`
);
}
informarMemoria('En arrencar');
// Simulem el cataleg d'un any sencer: 3000 sessions amb dades.
const sessions = [];
for (let i = 1; i <= 3000; i++) {
sessions.push({
id: `ses-${String(Math.ceil(i / 3)).padStart(3, '0')}-${(i % 3) + 1}`,
dataHora: '2026-10-03T20:00:00',
aforament: 420,
venudes: i % 420,
preuCentims: 2500
});
}
informarMemoria('Amb 3000 sessions');
// Alliberem la referencia: els objectes passen a ser escombraries recollibles.
sessions.length = 0;
informarMemoria("Un cop buidat l'array");En arrencar rss= 42.3 MB heapTotal= 5.6 MB heapUsed= 4.4 MB Amb 3000 sessions rss= 45.1 MB heapTotal= 8.4 MB heapUsed= 5.7 MB Un cop buidat l'array rss= 45.1 MB heapTotal= 8.4 MB heapUsed= 5.7 MB
Què mesura cada camp:
| Camp | Què mesura | Quan mirar-lo |
|---|---|---|
rss |
Resident Set Size: memòria física total del procés, inclòs el mateix binari de Node | Per saber quanta RAM demana el teu contenidor en producció |
heapTotal |
Munt de V8 reservat | Comparar amb heapUsed per veure el marge |
heapUsed |
Munt de V8 realment en ús pels teus objectes | La mètrica clau per detectar fuites de memòria |
external |
Memòria d'objectes C++ vinculats a JavaScript (buffers, sockets) | En treballar amb Buffer (Mòdul 3) |
arrayBuffers |
Part d'external corresponent a ArrayBuffer |
Dades binàries |
Fixa't en la tercera línia de l'exemple: la memòria no va baixar en buidar l'array. No és cap error. La recol·lecció d'escombraries no és immediata: V8 recuperarà aquest espai quan li convingui, no pas quan tu deixis de fer servir els objectes. Per això, per diagnosticar fuites, no miris mai una mesura aïllada: mira la tendència de heapUsed al llarg de minuts o hores. Una línia que puja i baixa és sana; una que només puja és una fuita.
Al Mòdul 11 convertirem aquesta mesura manual en mètriques publicades de manera contínua.
Errors Comuns i Consells
Error 1: creure que Node és literalment monofil i que per això no pot aprofitar diversos nuclis.
Mitja veritat. El teu JavaScript és monofil, però l'E/S de fitxers, el DNS i crypto sí que fan servir diversos nuclis a través del thread pool. El que no es paral·lelitza sol és el teu codi.
Error 2: fer servir les versions ...Sync en un servidor.
fs.readFileSync en un script d'una sola execució és perfectament raonable. Dins d'un gestor de peticions HTTP és un error greu: bloqueja tots els usuaris mentre el disc respon.
Error 3: pujar UV_THREADPOOL_SIZE a la brava.
Més fils que nuclis genera competència i canvis de context. Mesura abans de tocar-ho, i recorda que cal posar-ho abans d'arrencar el procés.
Error 4: confondre process.version amb process.versions.
La primera és una cadena ('v22.11.0'), la segona un objecte. process.versions.node sí que és equivalent a la primera, però sense la v.
Error 5: cridar process.exit() per «acabar bé».
Talla el procés en sec. Si hi havia un fitxer escrivint-se o una resposta HTTP a mig enviar, es perden. Fes servir process.exitCode i deixa que el bucle acabi sol.
Error 6: diagnosticar una fuita de memòria amb una sola mesura.
heapUsed puja i baixa constantment pel funcionament mateix del recol·lector. Només la tendència sostinguda és senyal de fuita.
Consell 1: memoritza la pregunta. Davant de qualsevol operació, pregunta't: «això és E/S o és càlcul?». Si és E/S, hi ha una versió asíncrona i l'has de fer servir. Si és càlcul llarg en JavaScript, no hi ha màgia: o l'optimitzes, o el mous a un worker, o el treus del procés.
Consell 2: tingues a mà l'script del batec. El setInterval que imprimeix cada 100 ms és la manera més barata de saber si alguna cosa està bloquejant el teu procés. A la lliçó següent el convertirem en una mesura formal del retard del bucle d'esdeveniments.
Consell 3: llegeix el codi font de Node. Com que bona part de la biblioteca estàndard és JavaScript, lib/fs.js o lib/events.js al repositori de Node són perfectament llegibles i ensenyen moltíssim.
Exercicis
Exercici 1: trobar la mida òptima del thread pool
Amplia src/laboratori/thread-pool.js perquè, a més del que ja fa, imprimeixi al final un resum: el temps total, el nombre de tasques i el temps mitjà per tasca. Després executa l'script amb 8 tasques i amb aquests valors d'UV_THREADPOOL_SIZE: 1, 2, 4, 8 i 16.
Omple una taula amb els temps totals, explica la forma de la corba i respon: per què passar de 8 a 16 no millora (i probablement empitjora)?
Pista: require('node:os').cpus().length et diu quants nuclis té la teva màquina. La resposta depèn d'aquest número.
Exercici 2: l'informe que congela Escena Viva
Escriu src/laboratori/informe-bloquejant.js que simuli el que passa al servidor d'Escena Viva durant l'obertura de venda d'un festival:
- Un
setIntervalcada 50 ms que imprimeixipeticio atesa en T ms(simula els usuaris comprant). - Als 500 ms, una funció síncrona
calcularOcupacioGlobal()que recorri un array de 3.000 sessions i, per cadascuna, faci un càlcul artificialment costós (per exemple, 200.000 iteracions d'una operació matemàtica) perquè el total trigui al voltant d'un segon. - El procés s'ha d'aturar als 3 segons.
Executa'l, compta quantes peticions es van perdre durant el bloqueig i calcula quants usuaris haurien quedat afectats si el servidor atengués 200 peticions per segon.
Exercici 3: un diagnòstic d'arrencada per a Escena Viva
Escriu src/utils/diagnostic.js, un mòdul de diagnòstic que l'aplicació executarà en arrencar i que imprimeixi per stderr un bloc amb:
- Versió de Node, de V8 i de libuv.
- Plataforma, arquitectura, nombre de nuclis i
pid. - Mida del thread pool efectiva.
- Memòria
rssiheapUseden MB amb un decimal. - Un avís si la versió major de Node és inferior a 20, amb
process.exitCode = 1. - Un avís si el nombre de nuclis és 1, indicant que
clusterno aportarà res (avançament del Mòdul 10).
Ha de funcionar tal qual amb node src/utils/diagnostic.js.
Solucions
Solució 1
// src/laboratori/thread-pool.js
const crypto = require('node:crypto');
const os = require('node:os');
const TASQUES = Number(process.argv[2]) || 5;
const ITERACIONS = 200000;
const FILS = Number(process.env.UV_THREADPOOL_SIZE) || 4;
const inici = Date.now();
let acabades = 0;
console.error(`Nuclis: ${os.cpus().length} | Thread pool: ${FILS} | Tasques: ${TASQUES}`);
for (let i = 1; i <= TASQUES; i++) {
crypto.pbkdf2('entrada-escena-viva', 'sal', ITERACIONS, 64, 'sha512', () => {
const transcorregut = Date.now() - inici;
console.log(`Tasca ${String(i).padStart(2)} acabada en ${transcorregut} ms`);
acabades++;
if (acabades === TASQUES) {
const total = Date.now() - inici;
console.log('');
console.log(`Total : ${total} ms`);
console.log(`Mitjana/tasca: ${Math.round(total / TASQUES)} ms`);
// Tandes teoriques: quantes rondes completes del pool han calgut.
console.log(`Tandes : ${Math.ceil(TASQUES / FILS)}`);
}
});
}for n in 1 2 4 8 16; do
echo "--- UV_THREADPOOL_SIZE=$n ---"
UV_THREADPOOL_SIZE=$n node src/laboratori/thread-pool.js 8 | tail -4
doneResultats típics en una màquina de 8 nuclis:
UV_THREADPOOL_SIZE |
Tandes | Total aproximat | Comentari |
|---|---|---|---|
| 1 | 8 | ~1400 ms | Tot en sèrie: 8 × 175 ms |
| 2 | 4 | ~700 ms | La meitat |
| 4 | 2 | ~350 ms | El valor per defecte |
| 8 | 1 | ~190 ms | Òptim: una sola tanda, un fil per nucli |
| 16 | 1 | ~210 ms | Lleugerament pitjor |
La corba baixa de manera gairebé proporcional fins a igualar el nombre de nuclis i després s'aplana o empitjora. La raó és que aquestes tasques són de CPU pura: amb 8 nuclis només es poden executar 8 càlculs simultanis de debò. Amb 16 fils, el sistema operatiu reparteix els mateixos 8 nuclis entre 16 fils alternant-los, i aquest repartiment té un cost (canvi de context, invalidació de memòria cau) sense cap guany. Més fils només ajuden quan els fils esperen en lloc de calcular, que és el cas de l'E/S de fitxers lenta, no pas el de pbkdf2.
Solució 2
// src/laboratori/informe-bloquejant.js
// Simula l'efecte d'un calcul sincron llarg sobre el transit d'Escena Viva.
const inici = Date.now();
let ateses = 0;
// Cada 50 ms atenem una peticio de compra.
const transit = setInterval(() => {
ateses++;
console.log(`peticio ${String(ateses).padStart(3)} atesa en ${Date.now() - inici} ms`);
}, 50);
// Cataleg gran: 3000 sessions.
const sessions = [];
for (let i = 1; i <= 3000; i++) {
sessions.push({ id: `ses-${i}`, aforament: 420, venudes: i % 420, preuCentims: 2500 });
}
// Calcul deliberadament costos: per cada sessio, 200000 operacions.
function calcularOcupacioGlobal(sessions) {
let acumulat = 0;
for (const sessio of sessions) {
let soroll = 0;
for (let i = 0; i < 200000; i++) {
soroll += Math.sqrt(i); // Treball artificial: representa un calcul real mal plantejat.
}
acumulat += sessio.venudes / sessio.aforament + soroll * 0;
}
return Math.round((acumulat / sessions.length) * 100);
}
setTimeout(() => {
const t0 = Date.now();
console.error(">> Comenca l'informe sincron");
const ocupacio = calcularOcupacioGlobal(sessions);
const duracio = Date.now() - t0;
console.error(`>> Informe acabat: ${ocupacio}% d'ocupacio, ${duracio} ms de bloqueig`);
console.error(`>> Peticions perdudes: ~${Math.floor(duracio / 50)}`);
}, 500);
setTimeout(() => {
clearInterval(transit);
console.error(`Total de peticions ateses en 3 s: ${ateses} (haurien de ser ~60)`);
}, 3000);Sortida resumida:
peticio 1 atesa en 51 ms ... peticio 9 atesa en 452 ms >> Comenca l'informe sincron >> Informe acabat: 50% d'ocupacio, 1180 ms de bloqueig >> Peticions perdudes: ~23 peticio 10 atesa en 1683 ms ... Total de peticions ateses en 3 s: 37 (haurien de ser ~60)
Anàlisi: es van perdre unes 23 ranures de 50 ms. Si el servidor real atengués 200 peticions per segon, un bloqueig d'1,18 segons hauria deixat en espera uns 236 usuaris, tots ells amb la roda girant al navegador encara que la seva compra no tingués res a veure amb l'informe. I no és que s'endarrerissin una mica: es van endarrerir més d'un segon, que en una obertura de venda és la diferència entre aconseguir entrada i no aconseguir-ne.
La solució no és «optimitzar l'informe» (encara que ajudi), sinó treure'l del fil principal: un worker_thread (Mòdul 10) o un treball en cua que el calculi fora i deixi el resultat a la memòria cau.
Solució 3
// src/utils/diagnostic.js
// Diagnostic d'arrencada d'Escena Viva.
// Tot va per stderr: es informacio d'operacio, no dades de l'aplicacio.
const os = require('node:os');
const MAJOR_MINIM = 20;
function enMegues(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function diagnosticar() {
const memoria = process.memoryUsage();
const nuclis = os.cpus().length;
const filsPool = Number(process.env.UV_THREADPOOL_SIZE) || 4;
const majorActual = Number(process.version.slice(1).split('.')[0]);
console.error('=========================================');
console.error(" ESCENA VIVA - DIAGNOSTIC D'ARRENCADA");
console.error('=========================================');
console.error(` Node.js : ${process.version}`);
console.error(` V8 : ${process.versions.v8}`);
console.error(` libuv : ${process.versions.uv}`);
console.error(` Plataforma : ${process.platform} ${process.arch}`);
console.error(` Nuclis : ${nuclis}`);
console.error(` Thread pool : ${filsPool} fils`);
console.error(` PID : ${process.pid}`);
console.error(` Memoria rss : ${enMegues(memoria.rss)}`);
console.error(` Heap usat : ${enMegues(memoria.heapUsed)}`);
console.error('-----------------------------------------');
if (majorActual < MAJOR_MINIM) {
console.error(` AVIS: cal Node ${MAJOR_MINIM}+. Executa "nvm use".`);
process.exitCode = 1;
} else {
console.error(' Versio de Node: correcta.');
}
if (nuclis === 1) {
console.error(' AVIS: un sol nucli. El modul cluster no aportara cap millora.');
}
console.error('=========================================');
}
diagnosticar();Detalls de la solució que convé notar:
- Tot per
stderr. Així pots fernode src/cataleg.js > cataleg.txti el diagnòstic es continuarà veient per pantalla sense embrutar el fitxer de dades. process.exitCodeen lloc deprocess.exit(), coherent amb la convenció del projecte.- L'avís d'un sol nucli és real: en un contenidor amb
--cpus=1, arrencar 4 processos ambclusterempitjora el rendiment en comptes de millorar-lo.
A la lliçó Mòduls CommonJS i require() convertirem aquest fitxer en un mòdul reutilitzable que exporti la funció diagnosticar en lloc d'executar-la en carregar-se. Ara mateix fa les dues coses, que és justament la mala pràctica que allà aprendrem a evitar.
Conclusió
Has obert la caixa. Node.js no és «JavaScript al servidor» i prou: és un programa en C++ que combina V8, el motor que compila i executa el teu JavaScript amb JIT i gestiona el munt i la recol·lecció d'escombraries, amb libuv, la biblioteca en C que aporta el bucle d'esdeveniments, l'E/S asíncrona del sistema operatiu i un thread pool. Entre tots dos i el teu codi hi ha dues capes més: la biblioteca estàndard —en bona part escrita en JavaScript— i els bindings de C++ que fan de frontera.
Has corregit el tòpic més repetit de l'ecosistema: Node no és d'un sol fil del tot. El teu JavaScript sí que corre en un únic fil —i d'aquí ve la tranquil·litat de no tenir condicions de carrera—, però les lectures de fitxer, les resolucions de dns.lookup, la criptografia i la compressió viatgen a un thread pool de quatre fils configurable amb UV_THREADPOOL_SIZE, mentre que la xarxa fa servir directament epoll, kqueue o IOCP sense consumir fils. I no t'ho has cregut: ho has mesurat amb crypto.pbkdf2, veient com quatre tasques acaben alhora i la cinquena espera el seu torn.
També has comprovat el pecat capital. Un bucle síncron d'un segon va deixar el procés sord durant un segon sencer: sense batecs, sense temporitzadors i —en un servidor real— sense atendre ningú. Aquesta és la raó per la qual a Escena Viva un informe pesat no es pot calcular dins d'una petició, i el motiu que el Mòdul 10 existeixi. Finalment, entens per què un script acaba sol: libuv compta les tasques pendents i, quan el compte arriba a zero, el procés emet exit i mor; un setInterval o un servidor escoltant mantenen aquest compte viu expressament, i unref() és la via d'escapament.
Queda una peça que hem esmentat a cada apartat sense desenvolupar: el bucle d'esdeveniments. Saps que existeix, que gira, que executa callbacks i que compta referències, però no saps en quin ordre fa les coses. I aquest ordre ho explica tot: per què setTimeout(fn, 0) no és immediat, per què setImmediate de vegades va abans que un temporitzador i de vegades després, i per què process.nextTick es cola davant de tothom. A la lliçó següent, El Bucle d'Esdeveniments (Event Loop), recorrerem les seves sis fases una a una fins que puguis predir, línia a línia, l'ordre de sortida de qualsevol programa asíncron.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
