Passa a:
Performant npm: il gestore di pacchetti che ripensa node_modules

Performant npm: il gestore di pacchetti che ripensa node_modules

La scelta del gestore di pacchetti è una delle poche decisioni tecniche che quasi nessun team prende in modo consapevole. Si eredita dal template usato per inizializzare il progetto, dal tutorial seguito il primo giorno oppure semplicemente dal fatto che npm arriva già installato insieme a Node e nessuno ha motivo di cercare altro. Poi il progetto cresce, le dipendenze si moltiplicano, la cartella node_modules diventa la struttura più pesante presente sul disco e ogni installazione pulita in integrazione continua consuma minuti che ormai vengono considerati inevitabili. pnpm nasce esattamente da questa insoddisfazione, e la sua premessa è che gran parte di quel costo non sia intrinseca all’ecosistema JavaScript ma dipenda da come i pacchetti vengono materializzati sul filesystem.

Il nome sta per performant npm e la descrizione che se ne trova più spesso, un gestore veloce ed efficiente nell’uso dello spazio su disco, restituisce solo la metà meno interessante della storia. La velocità è una conseguenza, non un obiettivo perseguito con micro ottimizzazioni. Dietro ci sono due decisioni di progetto che si sostengono a vicenda, uno store globale condiviso tra tutti i progetti della macchina e una struttura di node_modules che rifiuta di appiattire l’albero delle dipendenze. La prima decide quanto spazio e quanto tempo costa installare, la seconda decide che cosa il codice può importare, ed è quella che cambia davvero il modo di lavorare.

Parodia del diagramma della curvatura dello spaziotempo, con il Sole che produce un avvallamento leggero, la stella di neutroni uno più profondo, il buco nero uno molto profondo e node_modules un pozzo che esce dai bordi dell'immagine
Il meme che ogni sviluppatore JavaScript ha visto almeno una volta, ed è esattamente il problema da cui parte pnpm

Con npm ogni progetto riceve la propria copia fisica di ogni pacchetto, così che dieci progetti che usano la stessa versione di una libreria pagano dieci volte lo stesso contenuto. pnpm mantiene invece un unico store, indirizzato per contenuto, in cui ogni file viene salvato una sola volta e identificato dal proprio hash. Quando un progetto dichiara una dipendenza, pnpm non copia nulla ma crea degli hard link verso i file già presenti nello store, quindi lo stesso blocco su disco viene referenziato da tutte le installazioni che ne hanno bisogno e occupa spazio una volta sola.

Il fatto che l’indirizzamento avvenga a livello di file e non di pacchetto ha un effetto secondario notevole, perché aggiornare una libreria alla versione successiva comporta lo scaricamento dei soli file effettivamente cambiati mentre tutto il resto viene collegato a quello che c’era già. Installare diventa in buona parte un’operazione di collegamento sul filesystem invece che di copia e decompressione, ed è da qui che arriva la differenza di tempi rispetto agli altri gestori. L’unico vincolo da conoscere è che gli hard link funzionano all’interno dello stesso volume, quindi se lo store risiede su un disco diverso da quello del progetto pnpm ripiega sulla copia e il vantaggio si riduce.

La struttura non piatta di node_modules

La seconda decisione è quella che genera più discussioni. npm e Yarn nella sua versione classica appiattiscono l’albero delle dipendenze, sollevando i pacchetti transitivi al primo livello di node_modules per evitare duplicazioni. La conseguenza è che qualunque libreria installata da qualcun altro nella catena diventa importabile dal codice applicativo anche se non compare in package.json. Sono le cosiddette dipendenze fantasma, e il loro problema non è teorico, perché il giorno in cui una dipendenza intermedia cambia versione o smette di usare quella libreria il progetto smette di compilare senza che nessuno abbia toccato il proprio codice.

# In a pnpm project, only declared dependencies are visible at the top level
node_modules/
  express -> .pnpm/express@4.19.2/node_modules/express
  .pnpm/
    express@4.19.2/node_modules/express
    body-parser@1.20.2/node_modules/body-parser

pnpm rende impossibile quella scorciatoia. Al primo livello di node_modules compaiono soltanto i pacchetti dichiarati esplicitamente, sotto forma di collegamenti simbolici che puntano a uno store virtuale interno dove risiede l’albero reale con tutte le versioni risolte. Importare qualcosa che non è stato dichiarato produce un errore immediato, in fase di sviluppo, invece di un guasto differito in produzione.

Confronto tra il node_modules piatto di npm, dove express, body-parser, qs e debug stanno tutti al primo livello, e quello di pnpm, dove al primo livello resta solo il collegamento simbolico a express che punta allo store virtuale .pnpm e da lì allo store globale
Con npm ogni pacchetto transitivo risale al primo livello, con pnpm resta dietro un collegamento simbolico
I comandi di ogni giorno e la migrazione

L’interfaccia è volutamente familiare, al punto che la curva di apprendimento si esaurisce nel giro di una mattinata. Il comando pnpm install ricostruisce le dipendenze a partire dal lockfile, pnpm add aggiunge un pacchetto, pnpm remove lo elimina e pnpm run esegue uno script definito in package.json. Vale la pena conoscere anche pnpm dlx, che esegue un pacchetto senza installarlo come farebbe npx, e pnpm why, che spiega quale dipendenza sta trascinando dentro una certa libreria.

La migrazione di un progetto esistente è meno invasiva di quanto si tema. Il comando pnpm import legge il file package-lock.json già presente e ne deriva un pnpm-lock.yaml equivalente, preservando le versioni esatte già risolte, dopo di che si rimuovono il vecchio lockfile e la cartella node_modules e si reinstalla da zero. Conviene poi fissare la versione del gestore nel campo packageManager di package.json e attivare corepack, in modo che ogni sviluppatore e la pipeline di build usino esattamente lo stesso binario.

# Enable the version pinned in the packageManager field
corepack enable

# Derive a pnpm lockfile from an existing package-lock.json
pnpm import

# Add a dependency to a single package of the workspace
pnpm add zod --filter @app/api
I workspace e i monorepo

Il supporto ai monorepo è nativo e non richiede strumenti aggiuntivi. Un file pnpm-workspace.yaml alla radice elenca i percorsi dei pacchetti che compongono il repository, e da quel momento ognuno conserva il proprio package.json e il proprio node_modules senza che le dipendenze vengano sollevate. Ne deriva la stessa proprietà vista prima applicata su scala più grande, dato che un pacchetto non può usare per sbaglio una libreria dichiarata da quello accanto. Il protocollo workspace permette di riferirsi a un pacchetto interno senza passare dal registro pubblico, e in fase di pubblicazione il riferimento viene sostituito con il numero di versione reale. L’opzione filter consente infine di eseguire uno script su un sottoinsieme del repository, includendo su richiesta anche i pacchetti che dipendono da quello selezionato.

Dove pnpm crea attrito

Sarebbe disonesto presentarlo come una sostituzione priva di costi. Una parte dell’ecosistema dà per scontata la struttura piatta di node_modules, e strumenti legacy, alcuni bundler e certe configurazioni di React Native possono non risolvere i moduli come si aspettano. pnpm mette a disposizione delle vie di fuga graduali, dalla possibilità di sollevare pubblicamente solo i pacchetti indicati da un pattern fino a un linker che ricrea l’intera struttura appiattita mantenendo però lo store condiviso. Sono compromessi accettabili quando servono, purché restino l’eccezione e non la configurazione predefinita.

Il costo di migrazione, per un progetto sano, si misura in ore, mentre il beneficio è strutturale. Si recupera spazio su disco, si accorciano i tempi di installazione in integrazione continua e soprattutto si ottiene un grafo delle dipendenze che corrisponde a quello dichiarato. Il gestore di pacchetti non è una preferenza di gusto ma il livello che decide che cosa il codice può importare, e vale la pena sceglierlo con la stessa attenzione dedicata a qualunque altra scelta di architettura.

Per saperne di più: https://pnpm.io/

Ranieri Vannozzi

L'autore

Ranieri Vannozzi

Sviluppatore e consulente software freelance

Scrivo codice dal 2008 e lavoro su sviluppo enterprise, architetture cloud-native e leadership tecnica. Su HeroHubs racconto quello che incontro ogni giorno nei progetti, tra codice, architetture e intelligenza artificiale.

Articolo scritto con il supporto dell'AI e rivisto dall'autore.

Post Correlati

Conventional Commits: il messaggio di commit come documento tecnico

Sviluppo

Giugno 2026

Conventional Commits: il messaggio di commit come documento tecnico

Scrivere i messaggi di commit in modo libero è una convenzione che si forma per inerzia. Conventional Commits la sostituisce con una struttura che genera valore automaticamente.

Brute force protection: l'autenticazione che non cede

Sviluppo

Giugno 2026

Brute force protection: l'autenticazione che non cede

Un attacco brute force non è sofisticato ma è persistente. Costruire una difesa solida è una questione architetturale prima ancora che di configurazione.

Perché ignorare il package-lock è un suicidio architettonico?

Sviluppo

Maggio 2026

Perché ignorare il package-lock è un suicidio architettonico?

Molti sviluppatori considerano il lock file un rumore di fondo nei commit. In realtà, è l'unica cosa che separa il tuo codice da un crash improvviso in produzione.

Biome: la toolchain moderna che semplifica lo sviluppo web

Sviluppo

Ottobre 2025

Biome: la toolchain moderna che semplifica lo sviluppo web

Scopri Biome: la toolchain unificata per linting, formatting e analisi del codice JavaScript/TypeScript. Vantaggi, integrazione e produttività per sviluppatori moderni.

Test Driven Development: un alleato silenzioso per gli eroi del software

Sviluppo

Ottobre 2025

Test Driven Development: un alleato silenzioso per gli eroi del software

Scopri il Test Driven Development (TDD): vantaggi, ciclo Red-Green-Refactor, esempi pratici e differenze con BDD per migliorare la qualità e la manutenibilità del software.

Astro: architettura a isole e rendering ibrido per contenuti performanti

Sviluppo

Ottobre 2025

Astro: architettura a isole e rendering ibrido per contenuti performanti

Guida ad Astro: framework moderno per siti statici e ibridi, architettura a isole, rendering server-first, SEO, performance e integrazione con React, Vue e Svelte.

Feature Flags: la chiave per rilasci sicuri, controllati e continui

Sviluppo

Ottobre 2025

Feature Flags: la chiave per rilasci sicuri, controllati e continui

Scopri come le feature flags rivoluzionano il rilascio software: gestione graduale delle funzionalità, rollback istantanei, test A/B e best practice per DevOps e CI/CD.

Hono: semplicità e potenza per applicazioni web moderne

Sviluppo

Settembre 2025

Hono: semplicità e potenza per applicazioni web moderne

Guida a Hono: framework web minimalista per JavaScript, vantaggi, esempi pratici e come costruire applicazioni moderne, leggere e performanti.

Pull e Merge Request: stessi obiettivi ma approcci diversi

Sviluppo

Settembre 2025

Pull e Merge Request: stessi obiettivi ma approcci diversi

Pull request vs merge request: differenze, vantaggi e best practice per la revisione del codice su GitHub e GitLab, con focus su DevOps e collaborazione nei team di sviluppo.