«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#
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#
// 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#
// 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#
// 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#
// 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.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#
# 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.| Segnale | Vanilla | Framework |
|---|---|---|
| Pagine sostanzialmente statiche + un po' di interattività | sì | sovradimensionato |
| Moduli, validazione, componenti a fisarmonica, filtri | sì | ancora sovradimensionato |
| Un dato mostrato in 3+ punti che cambiano insieme | con render unico | comincia a convenire |
| Stato condiviso fra schermate diverse | faticoso | sì |
| Liste lunghe con aggiornamenti frequenti | no | sì (riconciliazione, virtualizzazione) |
| Componenti riusati in decine di posti con varianti | no | sì |
| Team numeroso su una base di codice condivisa | no | sì (convenzioni imposte) |
| Priorità assoluta a peso e tempo di avvio | sì | valutare 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.