«Per fare un sito serio serve React» è una delle convinzioni più diffuse dello sviluppo web contemporaneo. Come tutte le semplificazioni contiene qualcosa di vero — ci sono interfacce in cui un framework è chiaramente la scelta giusta — e nasconde la domanda utile: a partire da quale complessità?

Il prezzo del biglietto#

quanto costa includere un framework
Che cosa si paga davvero includendo un framework, prima ancora discrivere una riga di logica applicativa:  Trasferito (compresso)   Decompresso   Da interpretare ed eseguire  ──────────────────────   ───────────   ───────────────────────────  ~45 KB                   ~140 KB       ~140 KB di JavaScriptIl numero che conta non è il primo. È il terzo: il browser devedecomprimere, interpretare, compilare ed eseguire quel codice PRIMAche compaia qualcosa sullo schermo. Su un telefono di fascia mediaquel lavoro si misura in centinaia di millisecondi — e cade esattamentenella finestra in cui si misura il tempo di risposta all'interazione.Non è un argomento contro i framework. È il prezzo del biglietto,e va confrontato con quello che si compra.

Il dato che viene citato di solito — i kilobyte trasferiti — è il meno rilevante dei tre. Su una connessione moderna quaranta kilobyte compressi arrivano in fretta. Quello che conta è che il browser deve poi decomprimerli, interpretarli, compilarli ed eseguirli, e lo fa sul thread principale, lo stesso che deve dipingere la pagina e rispondere ai tocchi dell'utente.

Il segnale preciso#

carrello.jsJavaScript
// Il segnale che il vanilla non basta più. NON è il numero di pagine:// è la sincronizzazione manuale dello stesso dato in più punti.// ✗ Tre punti da aggiornare a mano, e il quarto che si aggiungerà domani.function aggiungiAlCarrello(articolo) {  carrello.push(articolo);  document.querySelector('#conteggio').textContent = carrello.length;  document.querySelector('#totale').textContent = formatta(somma(carrello));  document.querySelector('#riepilogo').innerHTML = righe(carrello);  document.querySelector('#vai-alla-cassa').disabled = carrello.length === 0;  // ...e in rimuoviDalCarrello() ne dimenticherete uno}// Quando questo schema compare in cinque funzioni diverse, state// reimplementando la reattività di un framework — peggio, senza test,// e a mano. È il momento di smettere.

Questa è la classe di bug per cui i framework sono stati inventati. Non è la complessità visiva, non è il numero di schermate: è lo stesso dato che deve comparire in più punti, e ogni azione che deve ricordarsi di aggiornarli tutti. Prima o poi una funzione ne dimentica uno, e l'interfaccia mostra un totale che non corrisponde al contenuto del carrello.

La via di mezzo che copre moltissimi casi#

stato.jsJavaScript
// La via di mezzo che copre moltissimi casi: una funzione di render// unica, chiamata dopo OGNI modifica dello stato. Non è React, ma// elimina proprio la classe di bug del blocco precedente.let stato = { carrello: [], filtro: '', caricamento: false };function aggiorna(modifiche) {  stato = { ...stato, ...modifiche };  render();}function render() {  const n = stato.carrello.length;  $('#conteggio').textContent = String(n);  $('#totale').textContent = formatta(somma(stato.carrello));  $('#vai-alla-cassa').disabled = n === 0;  $('#riepilogo').replaceChildren(...stato.carrello.map(riga));}// Ogni azione tocca SOLO lo stato. Non c'è modo di dimenticare un// aggiornamento, perché non ci sono aggiornamenti sparsi da ricordare.aggiungi = (a) => aggiorna({ carrello: [...stato.carrello, a] });

Non è reattività fine: a ogni modifica si ridisegna tutto ciò che dipende dallo stato. Ma elimina la classe di bug del blocco precedente, perché non ci sono più aggiornamenti sparsi da ricordare: le azioni toccano solo lo stato, e il DOM è una funzione dello stato.

Il limite si vede quando ridisegnare tutto diventa costoso — liste lunghe, animazioni, elementi con stato interno come un campo di testo che perde il cursore. Quel limite arriva più tardi di quanto si creda, e quando arriva è chiaro.

Comporre HTML senza aprire una falla#

template.jsJavaScript
// Comporre HTML senza framework, senza aprire una falla XSS.// ✗ innerHTML con dati non fidati: se il nome contiene <img onerror=...>//   avete appena eseguito codice di qualcun altro nella vostra pagina.el.innerHTML = `<li>${prodotto.nome}</li>`;// ✓ 1. textContent per il testo: non interpreta nulla, mai.const li = document.createElement('li');li.textContent = prodotto.nome;// ✓ 2. <template> per strutture ripetute: il markup resta nell'HTML,//      dove si legge, e si clona invece di ricostruirlo a stringhe.const t = document.querySelector('#riga-prodotto');function riga(p) {  const n = t.content.cloneNode(true);  n.querySelector('.nome').textContent = p.nome;  n.querySelector('.prezzo').textContent = formatta(p.prezzo);  n.querySelector('a').href = '/prodotti/' + encodeURIComponent(p.slug);  return n;}

innerHTML con dati che non controllate è la strada più diretta a una vulnerabilità XSS: un nome prodotto che contiene <img src=x onerror=...> diventa codice eseguito nella vostra pagina, con accesso ai cookie e alla sessione dell'utente.

textContent non interpreta nulla, mai: è la scelta corretta per qualunque testo. E l'elemento <template> risolve il caso delle strutture ripetute meglio delle stringhe — il markup resta nell'HTML, dove è leggibile e dove l'editor lo formatta, e si clona invece di ricostruirlo.

Delega degli eventi#

eventi.jsJavaScript
// Delega degli eventi: un solo ascoltatore per tutta la lista, invece// di uno per riga. Funziona anche per gli elementi aggiunti dopo,// e non lascia ascoltatori orfani quando il DOM viene ricostruito.document.querySelector('#lista').addEventListener('click', (e) => {  const bottone = e.target.closest('[data-azione]');  if (!bottone) return;  const { azione, id } = bottone.dataset;  if (azione === 'aggiungi') aggiungi(id);  if (azione === 'rimuovi') rimuovi(id);});// Con mille righe: 1 ascoltatore invece di 1000. E la ricostruzione// della lista non richiede di riattaccare niente.

Un solo ascoltatore sul contenitore, invece di uno per riga. Il vantaggio non è solo la memoria: funziona per gli elementi aggiunti dopo, e non lascia ascoltatori orfani quando la lista viene ricostruita — che è la causa più comune di perdite di memoria nei frontend scritti a mano.

Vite per il multi-pagina#

vite.config.jsJavaScript
// vite.config.js — configurazione per un sito multi-pagina,// che è il caso in cui il vanilla rende di più.import { defineConfig } from 'vite';import { resolve } from 'path';export default defineConfig({  build: {    rollupOptions: {      // Un punto di ingresso per pagina: chi apre /contatti non      // scarica il JavaScript del blog. È lo "code splitting" che      // nei framework richiede configurazione, e qui è la struttura.      input: {        home:     resolve(__dirname, 'index.html'),        blog:     resolve(__dirname, 'blog/index.html'),        contatti: resolve(__dirname, 'contatti/index.html')      }    },    target: 'es2020',    cssCodeSplit: true  }});

Il caso in cui l'approccio senza framework rende di più è proprio quello multi-pagina, e Vite lo supporta nativamente: un punto di ingresso per pagina significa che chi apre /contatti non scarica il JavaScript del blog. È la separazione del codice che nelle applicazioni a pagina singola richiede configurazione e componenti caricati pigramente, e qui è semplicemente la struttura del progetto.

Misurare invece di discutere#

misura
# Il confronto va fatto sui numeri, non sulle impressioni.npx vite builddist/assets/app-9f3a2c1e.js    17.42 kB │ gzip:  6.18 kBdist/assets/app-4b2e8d10.css   25.58 kB │ gzip:  6.02 kB# E il costo di ESECUZIONE, che il peso non racconta:# DevTools → Performance → registra un caricamento#   Scripting:  quanto tempo il thread principale passa a eseguire JS#   Rendering:  quanto a calcolare layout e stili# Su un progetto vanilla lo Scripting iniziale sta tipicamente# sotto i 50 ms; con un framework medio si parte da 150-300 ms# su hardware di fascia media.
Il pannello Performance dice quanto tempo il thread principale passa a eseguire JavaScript. È il numero che l'utente percepisce, e non compare in nessun grafico di peso del bundle.
SegnaleVanillaFramework
Pagine sostanzialmente statiche + un po' di interattivitàsovradimensionato
Moduli, validazione, componenti a fisarmonica, filtriancora sovradimensionato
Un dato mostrato in 3+ punti che cambiano insiemecon render unicocomincia a convenire
Stato condiviso fra schermate diversefaticoso
Liste lunghe con aggiornamenti frequentino (riconciliazione, virtualizzazione)
Componenti riusati in decine di posti con variantino
Team numeroso su una base di codice condivisano (convenzioni imposte)
Priorità assoluta a peso e tempo di avviovalutare il rendering lato server

La conclusione non è che i framework siano un eccesso: è che la domanda va posta al contrario. Non «quale framework uso», ma «quale problema sto comprando la soluzione». Se la risposta è «la sincronizzazione di stato condiviso e complesso», il framework si ripaga da solo. Se è «tutti lo usano», si sta pagando un prezzo senza ricevere la merce — ed è una scelta che si porta dietro anche il costo, poco discusso, di tenere aggiornate le dipendenze per tutta la vita del progetto.