
React Compiler: cosa cambia per chi scrive componenti oggi
Cosa fa davvero il React Compiler, quando useMemo e useCallback diventano superflui, e perché le Rules of React contano più di prima.
Negli articoli su questo blog ho passato pagine a spiegare l’ordine degli hook, quando serve useReducer invece di useState, come evitare re-render inutili con useCallback. Buona parte di quella disciplina manuale — non tutta — con il React Compiler diventa superflua. Capire esattamente dove passa il confine è quello che conta, perché continuare a scrivere useMemo ovunque per abitudine non fa più male, ma nemmeno più bene di quanto pensi.
Il problema che il Compiler risolve
React ri-renderizza un componente ogni volta che il suo stato o i suoi genitori cambiano, anche quando il risultato finale sarebbe identico. La soluzione manuale — useMemo per i valori calcolati, useCallback per le funzioni passate come prop, React.memo per i componenti — funziona, ma ha un costo che si paga in due modi: bisogna ricordarsi di applicarla ovunque serva, e bisogna mantenere corretti gli array di dipendenze mano a mano che il codice cambia. Un array di dipendenze incompleto è uno dei bug più comuni e più silenziosi in React: non lancia un errore, semplicemente mostra dati vecchi.
Il React Compiler nasce per togliere questa responsabilità dallo sviluppatore e spostarla a build time.
Cosa fa davvero il Compiler
È un plugin Babel (esiste anche una versione per SWC) che analizza staticamente il codice dei tuoi componenti e hook, e inserisce automaticamente la memoizzazione equivalente a useMemo/useCallback/React.memo dove serve — senza che tu la scriva.
// Quello che scrivi
function ProductCard({ product, onAdd }) {
const formattedPrice = formatCurrency(product.price);
return <Card price={formattedPrice} onAdd={() => onAdd(product.id)} />;
}
// Quello che, concettualmente, il Compiler produce a build time
function ProductCard({ product, onAdd }) {
const formattedPrice = useMemo(() => formatCurrency(product.price), [product.price]);
const handleAdd = useCallback(() => onAdd(product.id), [onAdd, product.id]);
return <Card price={formattedPrice} onAdd={handleAdd} />;
}
Non stai leggendo l’output reale (il Compiler genera un codice più a basso livello, non letteralmente hook), ma il concetto è questo: la memoizzazione che prima scrivevi a mano — e che spesso, onestamente, non scrivevi affatto perché richiedeva disciplina — ora è automatica e coerente su tutta la codebase.
ℹ️ Nota
Non è retroattivo per magia: il Compiler funziona sul codice che compila: va aggiunto alla pipeline di build (Babel, o l’integrazione con Vite/Next.js), non è un flag runtime. Se lavori con più pacchetti in un monorepo, ognuno che vuole i benefici deve passare dal Compiler nella propria build.
Le Rules of React non sono opzionali — anzi
Qui sta la parte che i post entusiasti sui social tendono a saltare. Il Compiler può memoizzare in sicurezza solo se il tuo codice rispetta le Rules of React: componenti e hook devono essere funzioni pure durante il render (niente mutazioni dirette di props o stato), e gli hook vanno chiamati sempre nello stesso ordine, senza condizioni. Sono esattamente le regole di cui parlo nell’articolo sugli errori più comuni con gli hook — con il Compiler, violarle non è più “solo” un bug a runtime, è anche un’ottimizzazione che può risultare scorretta in modi non ovvi.
⚠ Attenzione
Il Compiler non ti avvisa sempre, a volte fa solo un passo indietro. Se rileva codice che non riesce ad analizzare con sicurezza, il Compiler fa bail-out: salta silenziosamente l’ottimizzazione per quel componente invece di applicarla male. Il codice continua a funzionare come prima, semplicemente non guadagni nulla — e senza eslint-plugin-react-compiler attivo nel progetto, potresti non accorgertene affatto. Vale la pena installarlo fin dal primo giorno, non aggiungerlo “quando c’è tempo”.
Cosa NON risolve
Il Compiler ottimizza il render dei componenti React, non ogni possibile causa di lentezza. Non aiuta con:
- Stato mutabile esterno a React — uno store globale, una libreria di terze parti che tiene riferimenti mutabili — perché il Compiler ragiona solo su ciò che può analizzare staticamente dentro i tuoi componenti.
useEffectscritti male: se un effect ha dipendenze sbagliate o fa lavoro che dovrebbe stare nel render, il Compiler non lo riscrive per te.- Interazioni bloccanti sul thread principale in generale: se il tuo problema è un calcolo pesante sincrono, la risposta è ancora spostarlo, non solo memoizzarlo meglio.
E il codice vecchio, pieno di useMemo manuali?
Non serve una migrazione. Il Compiler riconosce la memoizzazione manuale esistente e non la duplica in modo dannoso — puoi lasciarla dov’è. Il cambiamento pratico è nel codice nuovo: nei progetti che ho avviato dopo aver adottato il Compiler, semplicemente non scrivo più useMemo/useCallback per la prima stesura. Li reintroduco a mano solo nei rari casi in cui il Compiler fa bail-out e il profiler mostra che quel punto specifico conta davvero.
💬 Opinione personale
La cosa che è cambiata di più non è la performance, è la leggibilità. Il beneficio più grande che ho notato non è tanto “il sito è più veloce” — la memoizzazione manuale, quando fatta bene, arrivava quasi allo stesso risultato. È che il codice torna leggibile: niente più array di dipendenze da controllare a ogni review, niente più componenti con metà della logica dedicata a “come evito che questo si ri-calcoli”.
Prossimi passi
Se il tuo codice gestisce stato complesso, vale la pena rivedere anche quando useReducer è la scelta giusta rispetto a più useState sparsi — il Compiler ottimizza entrambi gli approcci, ma la chiarezza del codice resta una scelta tua, non sua. E se gestisci eventi su window o listener personalizzati, la guida agli event handler e gli hook resta valida indipendentemente dal Compiler: quella è gestione del ciclo di vita, non memoizzazione.