Architettura: V8 e Modello I/O
Le fondamenta tecniche di Node.js: come il motore V8 e il sistema I/O lo rendono incredibilmente veloce.
La velocità di Node.js non è magica, ma il risultato di due pilastri: il motore V8 (esecuzione) e Libuv (gestione I/O).
Per padroneggiare Node.js non basta saper scrivere codice JavaScript; bisogna capire come questo codice viene eseguito “sotto il cofano”. In questo capitolo esploreremo i componenti che rendono Node.js incredibilmente performante.
Il Motore V8: Il Cuore Pulsante
Node.js non interpreta JavaScript riga per riga. Utilizza V8, lo stesso motore open source sviluppato da Google per Chrome.
V8 trasforma il codice JavaScript in codice macchina in tempo reale, permettendo prestazioni simili ai linguaggi compilati (come C++).
Gestisce automaticamente la memoria per te, liberando le risorse non più utilizzate senza interventi manuali.
V8 è scritto in C++. Questo significa che Node.js agisce come un “guscio” JavaScript attorno a un motore estremamente veloce scritto a basso livello.
Il layer Libuv: Il ponte verso l’OS
Mentre V8 esegue il codice, Node.js ha bisogno di interagire con il file system, la rete e i database. V8 non sa fare queste cose nativamente. Qui entra in gioco Libuv, una libreria scritta in C che funge da ponte.
Libuv è il componente che implementa il Modello I/O non bloccante, permettendo a Node.js di essere asincrono e scalabile.
Blocking vs Non-Blocking I/O
Questa è la differenza fondamentale che definisce Node.js rispetto ad altri ambienti tradizionali.
1. Modello Blocking (Bloccante)
In un modello bloccante, il programma si ferma e aspetta che l’operazione (es. lettura di un file) sia conclusa prima di passare alla riga successiva.
import fs from "node:fs"
// Il programma si ferma qui finché il file non è letto
const data = fs.readFileSync("/file.txt")
console.log(data)
2. Modello Non-Blocking (Non Bloccante)
Node.js delega l’operazione al sistema operativo e prosegue immediatamente. Quando l’operazione è pronta, viene eseguita una callback.
import fs from "node:fs"
// Node.js avvia la lettura e passa subito oltre
fs.readFile("/file.txt", (err, data) => {
if (err) throw err
console.log(data)
})
Sebbene le callback siano le fondamenta, per scrivere codice leggibile ed elegante oggi si utilizzano le Promises e la sintassi async/await.
import fs from "node:fs/promises"
try {
const data = await fs.readFile("/file.txt")
console.log(data)
} catch (err) {
console.error(err)
}
Mettiamolo alla prova: Sync vs Async
È ora di vedere la differenza con i nostri occhi. Creiamo una sottocartella per questi esperimenti:
# Dalla cartella progetti-nodejs creata in precedenza
mkdir architettura-test
cd architettura-test
Per ora non serve un package.json. Questi script semplici
funzionano anche senza configurazione.
Proviamo a scrivere un file in due modi diversi per osservare il comportamento del thread principale.
Esempio Sincrono (sync.js)
Crea il file con il tuo editor preferito oppure da terminale:
# Linux/macOS
touch sync.js
# Windows (PowerShell)
New-Item sync.js -ItemType File
Node.js deve finire la scrittura prima di poter eseguire qualsiasi altra riga di codice.
import fs from "node:fs"
const text = "A\n".repeat(10 ** 6)
fs.writeFileSync("file.txt", text, "utf8")
console.log("Scrittura completata.")
console.log("Altro codice.")
Output:
Scrittura completata.
Altro codice.
Esempio Asincrono (async.js)
Node.js avvia la scrittura “in background” e prosegue immediatamente.
import fs from "node:fs"
const text = "A\n".repeat(10 ** 6)
fs.writeFile("file.txt", text, "utf8", (err) => {
if (err) throw err
console.log("Scrittura completata.")
})
console.log("Altro codice.")
Output:
Altro codice.
Scrittura completata.
Nell’esempio asincrono, “Altro codice” appare prima. Node.js non è rimasto con le mani in mano ad aspettare il disco!
Anche se l’asincronia sembra “gratis”, Node.js impiega un minimo di tempo extra per coordinare i thread interni di Libuv. Tuttavia, poter gestire migliaia di utenti mentre il disco lavora è il vero vantaggio competitivo.
Consiglio: Abbiamo usato
10 ** 6righe per velocità. Se vuoi “sentire” il blocco nel caso sincrono, prova ad aumentare a10 ** 8.
Ora sai che V8 esegue il codice e Libuv gestisce l’I/O, ma chi coordina tutto questo senza bloccare il sistema? Nel prossimo articolo incontrerai l’ Event Loop: il direttore d’orchestra di Node.js.