7 min

Approccio del compilatore alle performance web: il punto di vista di Rich Harris

Uno sguardo pratico all'approccio compilatore per le performance web, confrontando framework pesanti in runtime con output a build-time e proponendo un semplice quadro decisionale.

Approccio del compilatore alle performance web: il punto di vista di Rich Harris

Perché alcune web app sembrano lente anche su dispositivi buoni

Gli utenti raramente descrivono la performance in termini tecnici. Dicono che l'app sembra pesante. Le pagine impiegano un attimo in più a mostrare qualcosa, i pulsanti rispondono in ritardo e azioni semplici come aprire un menu, digitare in una ricerca o cambiare scheda si inceppano.

I sintomi sono familiari: un primo caricamento lento (UI vuota o a metà), interazioni scattose (click che arrivano dopo una pausa, scorrimento a scatti) e spinner lunghi dopo azioni che dovrebbero essere istantanee, come salvare un form o filtrare una lista.

Gran parte di questo è costo a runtime. In termini semplici, è il lavoro che il browser deve fare dopo il caricamento della pagina per rendere l'app utilizzabile: scaricare altro JavaScript, analizzarlo, eseguirlo, costruire la UI, agganciare handler e poi continuare a fare lavoro ad ogni aggiornamento. Anche su dispositivi veloci c'è un limite a quanto JavaScript puoi spingere nel browser prima che l'esperienza inizi a rallentare.

I problemi di performance emergono anche più tardi. All'inizio l'app è piccola: poche schermate, dati leggeri, UI semplice. Poi il prodotto cresce. Il marketing aggiunge tracker, il design componenti più ricchi, i team aggiungono stato, funzionalità, dipendenze e personalizzazioni. Ogni cambiamento sembra innocuo da solo, ma il lavoro totale si accumula.

Per questo i team iniziano a considerare idee orientate al compilatore. L'obiettivo di solito non è raggiungere punteggi perfetti. È continuare a rilasciare senza che l'app diventi più lenta ogni mese.

Runtime-heavy vs compile-time: l'idea di base

La maggior parte dei framework frontend ti aiuta a fare due cose: costruire un'app e mantenere la UI sincronizzata con i dati. La differenza chiave è quando avviene la seconda parte.

Con un framework pesante in runtime, gran parte del lavoro avviene nel browser dopo il caricamento della pagina. Spedisci un runtime generico in grado di gestire molti casi: tracciare cambiamenti, decidere cosa aggiornare e applicare quegli aggiornamenti. Questa flessibilità può essere ottima per lo sviluppo, ma spesso significa più JavaScript da scaricare, analizzare ed eseguire prima che la UI sia pronta.

Con l'ottimizzazione a tempo di compilazione, gran parte di quel lavoro si sposta nella fase di build. Invece di inviare al browser un grande insieme di regole, lo strumento di build analizza i tuoi componenti e genera codice più diretto e specifico per l'app.

Un modello mentale utile:

  • Runtime-heavy è come spedire istruzioni e una cassetta degli attrezzi, e poi assemblare a casa del cliente.
  • Compile-time è come spedire parti già tagliate e etichettate, così l'assemblaggio è più rapido.

La maggior parte dei prodotti reali sta da qualche parte nel mezzo. Gli approcci compiler-first spediscono comunque del codice runtime (routing, fetching dei dati, animazioni, gestione degli errori). I framework runtime-heavy si appoggiano anche a tecniche di build (minificazione, code splitting, rendering server-side) per ridurre il lavoro client. La domanda pratica non è quale schieramento sia “giusto”, ma quale mix si adatta al tuo prodotto.

Rich Harris e perché «il framework è un compilatore» conta

Rich Harris è una delle voci più chiare dietro il pensiero frontend orientato al compilatore. Il suo argomento è semplice: fai più lavoro in anticipo così gli utenti scaricano meno codice e il browser fa meno lavoro.

La motivazione è pratica. Molti framework pesanti in runtime spediscono un motore general-purpose: logica dei componenti, reattività, diffing, scheduling e helper che devono funzionare per ogni possibile app. Quella flessibilità costa byte e CPU. Anche quando la tua UI è piccola, puoi comunque pagare il prezzo di un runtime grande.

Un approccio compilatore inverte il modello. Durante la build, il compilatore guarda i tuoi componenti reali e genera il codice specifico per aggiornare il DOM di cui hanno bisogno. Se un'etichetta non cambia mai, diventa semplice HTML. Se cambia solo un valore, viene emesso soltanto il percorso di aggiornamento per quel valore. Invece di spedire una macchina UI generica, spedisci l'output su misura per il tuo prodotto.

Questo spesso porta a un risultato semplice: meno codice di framework inviato agli utenti e meno lavoro fatto a ogni interazione. Si nota soprattutto sui dispositivi di fascia bassa, dove l'overhead a runtime diventa visibile rapidamente.

I compromessi rimangono rilevanti:

  • Alcuni pattern dinamici sono più difficili quando il compilatore non può capirli in anticipo.
  • La qualità del tooling conta di più (velocità di build, debugging, source map).
  • Le convenzioni dell'ecosistema possono differire, influenzando assunzioni e riuso.
  • Potresti comunque aver bisogno di helper a runtime, ma più piccoli e mirati.

Una regola pratica: se la tua UI è in gran parte determinabile a build time, un compilatore può generare output molto compatto. Se la UI è altamente dinamica o guidata da plugin, un runtime più pesante può essere più semplice.

Cosa può migliorare in pratica l'ottimizzazione a tempo di compilazione

L'ottimizzazione a build sposta il luogo dove avviene il lavoro. Più decisioni vengono prese durante la build e meno lavoro resta per il browser.

Un risultato visibile è meno JavaScript inviato. Bundle più piccoli riducono il tempo di rete, il tempo di parsing e il ritardo prima che la pagina risponda a un tap o click. Su telefoni di fascia media questo conta più di quanto molti team si aspettino.

I compilatori possono anche generare aggiornamenti DOM più diretti. Quando la build vede la struttura di un componente, può produrre codice che tocca solo i nodi DOM che realmente cambiano, senza tanti strati di astrazione ad ogni interazione. Questo rende aggiornamenti frequenti più scattanti, specialmente in liste, tabelle e form.

L'analisi a build-time può poi rafforzare tree-shaking e rimozione del codice morto. Il guadagno non è solo file più piccoli, ma meno percorsi di codice che il browser deve caricare ed eseguire.

L'hydration è un altro ambito dove le scelte di build possono aiutare. L'hydration è il passo in cui una pagina renderizzata dal server diventa interattiva attaccando handler e ricostruendo lo stato nel browser. Se la build può marcare ciò che necessita davvero interattività e ciò che no, puoi ridurre il lavoro al primo caricamento.

Come effetto collaterale, la compilazione spesso migliora anche lo scoping del CSS. La build può riscrivere nomi di classi, rimuovere stili non usati e ridurre perdite di stile tra componenti. Questo abbassa costi imprevisti man mano che la UI cresce.

Immagina una dashboard con filtri e una grande tabella di dati. Un approccio compiler-first può mantenere il caricamento iniziale più leggero, aggiornare solo le celle cambiate dopo un click su un filtro ed evitare di idratare parti della pagina che non diventano mai interattive.

Dove i framework runtime-heavy hanno ancora senso

Confronta web e mobile
Crea un'app Flutter dallo stesso flusso e testa le prestazioni su telefoni di fascia media.

Un runtime più grande non è automaticamente negativo. Spesso compra flessibilità: pattern decisi a runtime, molte componenti di terze parti e flussi di lavoro consolidati.

I framework heavy in runtime brillano quando le regole dell'interfaccia cambiano spesso. Se ti servono routing complessi, layout nidificati, form ricchi e un modello di stato profondo, un runtime maturo può sembrare una rete di sicurezza.

Il runtime paga per la convenienza

Un runtime aiuta quando vuoi che il framework gestisca molte cose mentre l'app è in esecuzione, non solo in fase di build. Questo può rendere i team più veloci giorno per giorno, anche se aggiunge overhead.

Vantaggi comuni includono un grande ecosistema, pattern familiari per stato e fetching dei dati, dev tools solidi, estendibilità in stile plugin e onboarding più semplice quando assumi persone che conoscono lo stack.

La familiarità del team è un costo e un beneficio reali. Un framework leggermente più lento con cui il team può consegnare con fiducia può battere un approccio più veloce che richiede riqualificazione, disciplina più rigida o tooling personalizzato per evitare trappole.

Quando l'overhead del runtime non è il collo di bottiglia

Molti reclami su app “lente” non sono causati dal runtime del framework. Se la tua pagina aspetta API lente, immagini pesanti, troppi font o script di terze parti, cambiare framework non eliminerà il problema principale.

Un pannello amministrativo interno dietro login spesso va bene anche con un runtime più grande, perché gli utenti sono su dispositivi potenti e il lavoro è dominato da tabelle, permessi e query backend.

“Abbastanza veloce” può essere l'obiettivo giusto nelle prime fasi. Se stai ancora verificando il valore del prodotto, mantieni alta la velocità di iterazione, stabilisci budget di base e affronta la complessità compiler-first solo quando hai prove che serva.

Performance vs velocità di iterazione: il vero compromesso nei team

La velocità di iterazione è il tempo al feedback: quanto rapidamente qualcuno può cambiare una schermata, eseguirla, vedere cosa è successo e correggerlo. I team che mantengono questo ciclo corto rilasciano più spesso e imparano più rapidamente. Per questo i framework runtime-heavy possono sembrare produttivi all'inizio: pattern familiari, risultati rapidi, tanto comportamento integrato.

Il lavoro di performance rallenta quel ciclo quando è fatto troppo presto o troppo diffusamente. Se ogni pull request si trasforma in dibattiti di micro-ottimizzazione, il team smette di rischiare. Se costruisci una pipeline complessa prima di sapere cosa è il prodotto, si perde tempo a lottare con il tooling invece di parlare con gli utenti.

Il trucco è mettersi d'accordo su cosa significa “abbastanza buono” e iterare dentro quei limiti. Un budget di performance ti dà quel vincolo. Non si tratta di inseguire punteggi perfetti, ma di limiti che proteggono l'esperienza mantenendo il progresso nello sviluppo.

Un budget pratico potrebbe includere:

  • La pagina diventa utilizzabile entro pochi secondi su telefoni di fascia media.
  • JavaScript per rotta rimane sotto una soglia concordata dal team.
  • Interazioni chiave (cerca, aggiungi al carrello, salva) rispondono rapidamente.
  • Nessuna singola feature può aggiungere più di una piccola quantità fissa di JS o CSS.

Se ignori la performance, di solito paghi dopo. Quando un prodotto cresce, la lentezza si lega a decisioni architetturali, non solo a piccoli ritocchi. Una riscrittura tardiva può significare congelare funzionalità, riqualificare il team e rompere flussi che prima funzionavano.

Il tooling compiler-first può spostare questo compromesso. Potresti accettare build leggermente più lente, ma riduci il lavoro fatto su ogni dispositivo, ad ogni visita.

Rivedi i budget man mano che il prodotto si dimostra. All'inizio proteggi le basi. Con il crescere del traffico e delle entrate, stringi i budget e investi dove le modifiche muovono metriche reali, non orgoglio.

Come misurare la performance senza perderti

Le discussioni sulla performance si complicano quando nessuno concorda su cosa significhi “veloce”. Scegli un piccolo insieme di metriche, scrivile e trattale come un tabellone condiviso.

Un set iniziale semplice:

  • Tempo di caricamento iniziale (tempo al primo schermo utilizzabile)
  • Ritardo nelle interazioni (quanto rimane bloccato un tap o click)
  • Dimensione del bundle (cosa il browser deve scaricare e parsare)
  • Latencia delle API (quanto impiega il backend a rispondere)

Misura su dispositivi rappresentativi, non solo sul laptop di sviluppo. CPU veloci, cache calde e server locali possono nascondere ritardi che emergono su un telefono di fascia media in rete mobile.

Mantieni il tutto pragmatico: scegli due o tre dispositivi che rispecchiano gli utenti reali e esegui lo stesso flusso ogni volta (schermata home, login, un'attività comune). Fai questo con costanza.

Prima di cambiare framework, cattura una baseline. Prendi la build di oggi, registra i numeri per gli stessi flussi e tieni quella fotografia di riferimento.

Non giudicare la performance da un singolo punteggio di laboratorio. Gli strumenti di laboratorio aiutano, ma possono premiare la cosa sbagliata (ottimo primo caricamento) ignorando ciò di cui gli utenti si lamentano (menu scattosi, digitazione lenta, ritardi dopo la prima schermata).

Quando i numeri peggiorano, non indovinare. Controlla cosa è stato rilasciato, cosa ha bloccato il rendering e dove è stato speso il tempo: rete, JavaScript o API.

Un quadro decisionale passo-passo che puoi davvero usare

Ottieni codice verificabile
Trasforma UI e flussi di dati in codice sorgente che puoi ispezionare e ottimizzare.

Per fare una scelta calma e ripetibile, tratta le decisioni su framework e rendering come decisioni di prodotto. L'obiettivo non è la tecnologia migliore, ma il giusto equilibrio tra performance e ritmo di lavoro del team.

  1. Definisci le superfici del prodotto. Separa pagine marketing, dashboard autenticato, mobile e strumenti interni. Ogni superficie ha utenti, dispositivi e aspettative diverse.
  2. Stabilisci un budget di performance. Scegli 2 o 3 numeri da proteggere, ad esempio il primo caricamento su un telefono di fascia media e il tempo per un'interazione chiave (cerca, filtra, salva).
  3. Classifica i vincoli. Sii onesto su cosa conta di più ora: competenze del team, scadenze, SEO, regole di privacy dei dati e dove l'app deve funzionare.
  4. Scegli un approccio. Compiler-first è adatto quando primo caricamento e velocità delle interazioni sono non negoziabili. Runtime-first quando ti serve massima flessibilità e iterazione rapida. L'ibrido spesso vince: mantieni leggere le superfici sensibili alla performance e usa runtime più pesanti dove danno un chiaro vantaggio.
  5. Valida con una thin slice, poi consolidala. Costruisci un flusso utente reale end-to-end, misuralo rispetto al budget e documenta cosa hai scelto e perché.

Una thin slice dovrebbe includere le parti impegnative: dati reali, autenticazione e la tua schermata più lenta.

Se vuoi un modo rapido per prototipare quella thin slice, Koder.ai ti permette di costruire flussi web, backend e mobile via chat e poi esportare il codice sorgente. Questo ti aiuta a testare una rotta reale presto e mantenere gli esperimenti reversibili con snapshot e rollback.

Documenta la decisione in linguaggio semplice, incluso cosa ti farebbe rivederla (crescita del traffico, quota mobile, obiettivi SEO). Così la scelta resta valida anche quando il team cambia.

Errori comuni che i team fanno nelle scelte di performance

Le decisioni sulla performance di solito vanno storte quando i team ottimizzano ciò che vedono oggi, non ciò che gli utenti sentiranno tra tre mesi.

Un errore è over-ottimizzare nella prima settimana. Un team passa giorni a limare millisecondi su una pagina che cambia ancora quotidianamente, mentre il vero problema è che gli utenti non hanno ancora le funzionalità giuste. All'inizio, accelera l'apprendimento. Blocca il lavoro di performance profondo quando rotte e componenti si stabilizzano.

Un altro errore è ignorare la crescita dei bundle finché non diventa un problema. Le cose vanno bene a 200 KB, poi qualche aggiunta “piccola” dopo ti porta a megabyte. Un'abitudine semplice aiuta: traccia la dimensione dei bundle nel tempo e tratta i salti improvvisi come bug.

I team spesso scelgono rendering solo client-side per tutto, anche quando alcune rotte sono per lo più statiche (pagine pricing, documentazione, onboarding). Quelle pagine possono essere servite con molto meno lavoro sul dispositivo.

Un killer più silenzioso è aggiungere una grande libreria UI per comodità senza misurare il suo costo nelle build di produzione. La comodità è valida. Sii solo chiaro su cosa stai pagando in JavaScript, CSS extra e interazioni più lente su telefoni di fascia media.

Infine, mescolare approcci senza confini chiari crea app difficili da debug. Se metà dell'app si aspetta aggiornamenti generati dal compilatore mentre l'altra metà si affida a magie a runtime, ottieni regole poco chiare e errori confusi.

Alcune linee guida che funzionano nei team reali:

  • Imposta un budget di dimensione del bundle per rotta principale, non solo per tutta l'app.
  • Decidi quali rotte devono essere veloci al primo caricamento e progetta il rendering attorno a quelle.
  • Aggiungi librerie solo dopo averne verificato il peso in produzione.
  • Scrivi confini chiari (quali parti sono ottimizzate dal compilatore, quali usano il runtime).
  • Rivedi le decisioni dopo i dati utenti reali, non prima.

Un esempio realistico: lanciare una SaaS senza chiudersi in un vicolo cieco

Pubblica il primo flusso funzionante
Crea un'app web con React, un'API in Go e un backend Postgres senza impostare boilerplate.

Immagina un team di 3 persone che costruisce una SaaS per scheduling e fatturazione. Ha due facce: un sito marketing pubblico (landing, pricing, docs) e una dashboard autenticata (calendario, fatture, report, impostazioni).

Con una strada runtime-first scelgono una configurazione pesante in runtime perché rende rapide le modifiche UI. La dashboard diventa una grande app client-side con componenti riusabili, una libreria di stato e interazioni ricche. L'iterazione è veloce. Col tempo il primo caricamento comincia a sentirsi pesante su telefoni di fascia media.

Con una strada compiler-first scelgono un framework che sposta più lavoro al build time per ridurre JavaScript client. I flussi comuni come aprire la dashboard, cambiare schede e cercare risultano più scattanti. Il compromesso è che il team deve essere più deliberato su pattern e tooling, e alcune scorciatoie runtime non sono così plug-and-play.

Il cambiamento è raramente per gusto: è solitamente pressione dalla realtà: pagine più lente riducono le iscrizioni, più utenti arrivano su dispositivi economici, clienti enterprise chiedono budget prevedibili, la dashboard resta aperta a lungo e la memoria conta, o i ticket di supporto citano lentezza su reti reali.

Un'opzione ibrida spesso vince. Mantieni le pagine marketing leggere (server-rendered o per lo più statiche, codice client minimo) e accetta più runtime nella dashboard dove l'interattività ripaga.

Usando i passaggi decisionali: nominano i percorsi critici (iscrizione, prima fattura, report settimanale), li misurano su un telefono di fascia media, fissano un budget e scelgono l'ibrido. Default compiler-first per pagine pubbliche e componenti condivisi, runtime-heavy solo dove accelera chiaramente l'esperimentazione.

Checklist rapida e prossimi passi per il tuo prodotto

Il modo più semplice per rendere reali queste idee è un loop settimanale corto.

Inizia con una scansione di 15 minuti: la dimensione del bundle sta crescendo? Quali rotte sembrano lente? Quali sono i maggiori pezzi UI su quelle rotte (tabelle, grafici, editor, mappe)? Quali dipendenze pesano di più? Poi scegli un collo di bottiglia che puoi risolvere senza riscrivere lo stack.

Per questa settimana, mantieni piccolo l'impegno:

  • Misura una baseline: un cold load della tua rotta più lenta e una azione comune.
  • Stabilisci un budget che si adatti al tuo prodotto.
  • Risolvi un collo di bottiglia (rimuovi una dipendenza, separa una rotta, lazy-load un componente pesante, riduci render non necessari).
  • Misura di nuovo e annota cosa è cambiato.

Per mantenere le scelte reversibili, definisci confini chiari tra rotte e funzionalità. Preferisci moduli che puoi sostituire in seguito (grafici, editor rich text, SDK analitici) senza toccare tutta l'app.

Domande frequenti

Perché una web app sembra lenta anche se il mio dispositivo è veloce?

La maggior parte delle volte non è solo la rete: è il costo a runtime: il browser che scarica, analizza ed esegue JavaScript, costruisce l'interfaccia e continua a fare lavoro ad ogni aggiornamento.

Per questo un'app può risultare “pesante” anche su un buon laptop quando il carico JavaScript diventa grande.

Qual è la differenza tra framework pesanti in runtime e ottimizzazione a tempo di compilazione?

Hanno lo stesso obiettivo (far fare meno lavoro al client), ma il meccanismo è diverso.

  • Runtime-heavy: si invia un motore generico che decide gli aggiornamenti nel browser.
  • Compile-time: la fase di build genera codice di aggiornamento specifico per l'app, così il browser ha meno logica da eseguire.
Cosa significa realmente “il framework è un compilatore”?

Significa che il framework può analizzare i tuoi componenti in fase di build e produrre codice su misura per la tua app, invece di spedire un grande motore UI generico.

Il vantaggio pratico è solitamente bundle più piccoli e meno lavoro CPU durante le interazioni (click, digitazione, scorrimento).

Quali metriche di performance dovrei tenere d'occhio senza perdere la bussola?

Inizia con:

  • Tempo al primo schermo utilizzabile (non solo “pagina caricata”)
  • Ritardo nelle interazioni (un tap/click sembra bloccato?)
  • Dimensione JS per rotta (cosa il browser deve scaricare e parsare per ogni schermata)
  • Latencia API (tempo di risposta del backend)

Misura lo stesso flusso utente ogni volta così puoi confrontare le build in modo affidabile.

Cambiare framework può risolvere un'app lenta?

Può aiutare, ma prima verifica dove va davvero il tempo: rete, immagini pesanti, font, script di terze parti o CPU JavaScript. Se il collo di bottiglia non è il runtime del framework, cambiare framework non risolverà il problema principale.

La scelta del framework è una leva fra molte: misurate prima dove passa il tempo (network, CPU, rendering o backend).

Quando ha ancora senso usare un framework pesante in runtime?

Scegli runtime-heavy quando ti servono flessibilità e velocità di iterazione:

  • molte componenti di terze parti
  • pattern complessi di routing/layout
  • estensioni in stile plugin
  • team già esperto nell'ecosistema

Se il runtime non è il collo di bottiglia, la comodità può valere qualche byte in più.

Come scelgo tra compiler-first, runtime-first o un approccio ibrido?

Una regola pratica:

  • Compiler-first per le rotte che devono caricarsi e rispondere velocemente su telefoni di fascia media (landing, onboarding, flussi chiave).
  • Runtime-heavy dove serve massima flessibilità (dashboard complesse, strumenti interni).

L'ibrido funziona meglio se scrivi regole chiare, così l'app non diventa un miscuglio confuso di assunzioni.

Qual è un budget di performance pratico per un team reale?

Usa un budget che protegga la sensazione utente senza bloccare la release. Per esempio:

  • Primo schermo utilizzabile entro pochi secondi su un telefono di fascia media
  • Un tetto per JS per rotta
  • Azioni chiave (cerca, filtra, salva) che rispondono rapidamente
  • Nessuna singola feature può aggiungere più di una piccola quantità fissa di JS/CSS

I budget sono guide, non una gara per il punteggio perfetto.

Cos'è l'hydration e perché può rallentare il primo caricamento?

L'hydration è il lavoro necessario per rendere interattiva una pagina renderizzata dal server: attaccare handler e ricostruire lo stato nel browser.

Se si idrata troppo, il primo caricamento può sembrare lento anche se l'HTML è subito visibile. La build può ridurre l'hydration segnando ciò che davvero deve essere interattivo.

Cosa dovrei includere in un prototipo 'thin-slice' prima di scegliere un framework?

Un buon “thin slice” dovrebbe includere il lato reale e difficile:

  • autenticazione/login
  • dati reali e chiamate API lente
  • la schermata più lenta (tabelle, form, dashboard)
  • un'azione chiave ripetuta dagli utenti (filtra, salva, cerca)

Se prototipi questa slice, Koder.ai può aiutarti a costruire il percorso web + backend via chat ed esportare il codice sorgente, così puoi misurare e confrontare approcci senza impegnarti in un rewrite completo.

Related posts