I limiti del React Compiler sono nei componenti che salta

Immagina il rilascio. Il team scrive reactCompiler: true in next.config.ts, la build passa, e una pull request cancella ogni useMemo e useCallback del repository. Due settimane dopo un campo di un form con react-hook-form smette di mostrare quello che l'utente digita, e un effect di analytics parte a ogni render. Nell'output della build non c'era niente che puntasse a nessuno dei due.
I limiti del React Compiler sono silenziosi. Il React Compiler è uno strumento che lavora in fase di build e aggiunge da solo la memoizzazione a componenti e hook, stabile dalla versione 1.0 del 7 ottobre 2025. Quando incontra codice che non riesce a dimostrare sicuro, lo salta e continua la build, e con le impostazioni di default non lo segnala. I limiti veri stanno quindi in due punti: i componenti che il compilatore non ha mai ottimizzato, e quelli che ha ottimizzato ma che per funzionare contavano sulla memoizzazione.
Com'è fatto l'errore in un codice reale
Il rilascio qui sopra poggia su tre presupposti. Sono tutti falsi, e la documentazione di React spiega perché.
- Adesso ogni componente è compilato. Nella modalità
infer, quella di default, il compilatore prende solo le funzioni che hanno il nome di un componente (PascalCase) o di un hook (prefissouse) e che creano JSX o chiamano hook. EpanicThresholdvalenonedi default: ogni funzione che non riesce a compilare viene saltata e la build va avanti. - Il codice compilato si comporta come prima. La guida al debug di React indica il codice che dipende dalla memoizzazione per funzionare come uno dei modi principali in cui il compilatore può rompere un'app.
- La memoizzazione manuale è peso morto. La documentazione di React consiglia di lasciare al loro posto gli
useMemoe gliuseCallbackesistenti, perché toglierli può cambiare quello che il compilatore produce.
Quando la memoizzazione scritta a mano conviene ancora l'abbiamo spiegato nell'articolo precedente sul compilatore. Qui parliamo del codice che il compilatore non raggiunge, e di come vederlo.
Quando il React Compiler salta un componente?
Il compilatore è pensato per il codice che rispetta le Rules of React. Quando un componente rischia di violarle, la documentazione dice che il compilatore salta l'ottimizzazione invece di rischiare di cambiare il comportamento dell'app. Il componente gira lo stesso, senza memoizzazione, esattamente come prima che il compilatore fosse attivo. Quasi tutti i salti nascono da quattro schemi.
Ref letti o scritti durante il render
Leggere ref.current nel corpo del render, o assegnargli un valore, viola la regola per cui il render resta puro. Il lint refs segnala entrambi i casi. Il compilatore riconosce un ref da useRef(), da una prop ref in JSX o da qualunque nome che finisce in Ref e tocca .current. C'è un'eccezione ammessa: l'inizializzazione pigra, quando riempi ref.current una volta sola finché vale null. Per il resto si sposta la lettura in un effect o in un gestore di eventi, oppure si trasforma il valore in stato se l'interfaccia ne dipende.
Librerie con mutabilità interna
Alcune librerie restituiscono un oggetto o una funzione il cui riferimento resta uguale mentre il contenuto cambia. La memoizzazione controlla il riferimento, quindi il valore sembra congelato. Il lint incompatible-library cita due esempi: watch di react-hook-form e useReactTable di TanStack Table. Il compilatore salta i componenti che chiamano le API della sua lista. L'observer di MobX viola lo stesso presupposto, e la documentazione ammette che il linter non lo rileva ancora: un codice basato su MobX può essere compilato e comportarsi male. Con react-hook-form la soluzione è useWatch, che restituisce un valore con un riferimento nuovo quando il campo cambia.
Funzioni che non sembrano React
Una funzione di supporto chiamata calculateTotal non viene compilata in modalità infer, per quanto costi eseguirla. Lo stesso vale per un componente scritto con l'iniziale minuscola. Anche quando un calcolo pesante sta dentro un componente, l'introduzione è chiara: il compilatore memoizza solo componenti e hook, e la memoizzazione non è condivisa tra loro. La stessa chiamata costosa in cinque componenti gira cinque volte.
Effetti collaterali durante il render
Un render che scrive nel DOM, modifica un valore ricevuto o chiama qualcosa di non deterministico esce da quello che il compilatore può considerare puro. Le regole purity e immutability di eslint-plugin-react-hooks li segnalano. Erano bug latenti già prima del compilatore. Il compilatore li rende visibili, oppure ci gira intorno.
Quando il codice compilato si comporta in modo diverso?
Il salto è il guasto sicuro. Quello pericoloso è un componente che il compilatore ha compilato davvero, scritto però su un presupposto che il compilatore non condivide. La pagina di troubleshooting di React ne elenca tre forme:
- Effect che contano sull'uguaglianza dei riferimenti. Un effect dipende da un oggetto o da un array e si aspetta lo stesso riferimento da un render all'altro.
- Array di dipendenze che vogliono riferimenti stabili. Una dipendenza instabile fa partire l'effect troppo spesso, o in loop.
- Logica che decide in base a un confronto di riferimenti. Codice che confronta
prev === nextper decidere se ricalcolare o ricaricare.
I sintomi sono effect che partono troppe volte, loop infiniti e aggiornamenti che non arrivano mai. La documentazione propone un test pulito. Togli tutta la memoizzazione manuale dal componente e fallo girare senza compilatore. Se il bug resta, il componente viola una Rule of React e il compilatore l'ha solo messo in luce. Se il bug sparisce, il codice dipendeva dalla memoizzazione per essere corretto, ed è quella dipendenza che va tolta.
Next.js aggiunge un'impostazione da conoscere. enablePreserveExistingMemoizationGuarantees decide se il compilatore mantiene le garanzie degli useMemo, useCallback e React.memo già presenti. La documentazione di Next.js avverte che il valore di default può cambiare tra implementazioni e versioni del compilatore, e consiglia di impostarlo in modo esplicito. Un aggiornamento che lo inverte cambia il comportamento proprio nei componenti descritti sopra.
Cosa il compilatore lascia a te, per scelta
Alcuni limiti sono decisioni, scritte nella documentazione.
- Lavora sui re-render. La memoizzazione del compilatore punta alle prestazioni in aggiornamento: evitare re-render a cascata e calcoli ripetuti. Un primo caricamento lento per colpa del bundle, delle immagini o del fetch dei dati è un altro problema, quello che analizziamo in perché un sito è lento.
- I guadagni sono misurati sull'app di qualcun altro. Sul Meta Quest Store il team di React riporta caricamenti iniziali e navigazioni tra pagine fino al 12% più veloci e alcune interazioni più di 2,5 volte più rapide, con memoria invariata. Lo stesso articolo avverte che i risultati possono variare. Misura le tue schermate.
- Costa tempo di build. Il compilatore gira come plugin Babel. Next.js filtra con SWC e lo applica solo ai file con JSX o hook, e mette comunque in conto build un po' più lente rispetto al suo compilatore Rust di default.
- I Server Component ne ricavano poco. Un Server Component fa il render una volta per richiesta e non si ri-renderizza nel browser, quindi la memoizzazione del compilatore serve ai componenti client. Dove passa quel confine lo spieghiamo negli appunti sui Server Component in produzione.
Come trovare i componenti saltati in un codice esistente
Il numero da conoscere è la copertura, e la build di default non lo stampa. Ci si arriva in cinque passi.
- Attiva prima il lint. Dalla 1.0 le regole del compilatore sono in
eslint-plugin-react-hooks, nel presetrecommended. Il linter non richiede il compilatore installato, quindi puoi farlo girare su un codice prima di attivare qualsiasi cosa. Ogni segnalazione è un componente che il compilatore salterà o un bug che metterà in luce. - Registra cosa fa il compilatore. Dove la build usa direttamente il plugin Babel (Vite, Metro, una configurazione Babel semplice), aggiungi un
logger. EmetteCompileSuccess,CompileErroreCompileSkipper ogni funzione. Contali per cartella e hai un report di copertura. - Controlla in DevTools le schermate che contano. React DevTools segna un componente compilato con il badge Memo ✨. Apri le cinque schermate dove gli utenti passano più tempo e cerca i componenti che non ce l'hanno.
- Isola un bug a runtime con
"use no memo". La direttiva esclude una funzione, o un file intero se sta in cima. La documentazione la definisce uno strumento di debug temporaneo, quindi accompagnala sempre con un commento che dice perché e rimanda alla issue. - Sostituisci le API incompatibili una alla volta.
watchdiventauseWatch. La lettura di un ref nel render si sposta in un effect. Ogni correzione riporta un componente sotto il compilatore.
Come evitare che si ripresenti
La prevenzione è quasi tutta configurazione, da impostare una volta.
- Lint come errore in CI. Una pull request che aggiunge la lettura di un ref nel render fallisce prima della review.
- Una soglia più severa solo in sviluppo.
panicThreshold: 'critical_errors'durante il lavoro in locale fa emergere i fallimenti di compilazione. In produzione restanone, come chiede la documentazione, così un problema del compilatore non blocca mai un deploy. - Rilascio per cartelle. La guida all'adozione graduale propone tre strade: override di Babel per cartella,
compilationMode: 'annotation'con un"use memo"esplicito, oppure un controllo a runtime dietro un feature flag per un confronto A/B. - Memoizzazione manuale dove un effect ne dipende. Nel codice nuovo lasciamo decidere al compilatore e aggiungiamo
useMemosolo dove un valore alimenta un effect. La memoizzazione esistente resta finché un test non dimostra che si può togliere, e il lintpreserve-manual-memoizationverifica che le sue dipendenze siano complete. - Fissa l'impostazione di Next.js. Scrivi
enablePreserveExistingMemoizationGuaranteesnella configurazione con il valore che hai testato, così un aggiornamento non lo cambia al posto tuo.
Sulla maggior parte dei codici React il compilatore si guadagna il suo posto. I suoi limiti sono i componenti che non ha mai toccato e quelli che funzionavano solo perché qualcosa era memoizzato. Si trovano entrambi in un pomeriggio con il lint, un logger e DevTools, e restano invisibili a chi legge solo il log della build.
Domande frequenti
Nel codice esistente no, almeno non tutti insieme. La documentazione di React consiglia di lasciare la memoizzazione già presente, perché toglierla può cambiare quello che il compilatore produce, oppure di testare con cura prima di ogni rimozione. Nel codice nuovo scrivi valori e funzioni semplici e lascia memoizzare il compilatore. Tieni useMemo o useCallback dove un valore è una dipendenza di un effect e deve mantenere lo stesso riferimento: è il caso che la documentazione cita ancora come motivo per un controllo manuale.
Un po'. Il compilatore gira come plugin Babel, mentre Next.js di solito compila con SWC, il suo compilatore scritto in Rust. Per contenere il costo, Next.js usa un passaggio SWC che applica il React Compiler solo ai file con JSX o hook. La documentazione di Next.js parla comunque di build leggermente più lente rispetto al default, con un impatto piccolo e circoscritto. Misura il tempo di build in CI prima e dopo, invece di fidarti di un numero generico.
Sì. Il team di React dichiara il compilatore compatibile da React 17 in su. Con React 17 o 18 imposti un target minimo nella configurazione del compilatore e aggiungi il pacchetto react-compiler-runtime tra le dipendenze. Con React 19 non serve nessuno dei due passaggi. Le regole di salto sono le stesse in ogni versione, quindi il lint e i controlli di copertura di questo articolo servono anche su un codice più vecchio.
Articoli correlati
9 settembre 2026 · 7 min di lettura
23 aprile 2026 · 7 min di lettura
18 aprile 2026 · 7 min di lettura