Vai al contenuto

I limiti del React Compiler sono nei componenti che salta

Web · 11 novembre 2026 · 8 min di lettura

Cells of black, white and grey packed edge to edge

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 (prefisso use) e che creano JSX o chiamano hook. E panicThreshold vale none di 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 useMemo e gli useCallback esistenti, 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:

  1. 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.
  2. Array di dipendenze che vogliono riferimenti stabili. Una dipendenza instabile fa partire l'effect troppo spesso, o in loop.
  3. Logica che decide in base a un confronto di riferimenti. Codice che confronta prev === next per 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.

  1. Attiva prima il lint. Dalla 1.0 le regole del compilatore sono in eslint-plugin-react-hooks, nel preset recommended. 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.
  2. Registra cosa fa il compilatore. Dove la build usa direttamente il plugin Babel (Vite, Metro, una configurazione Babel semplice), aggiungi un logger. Emette CompileSuccess, CompileError e CompileSkip per ogni funzione. Contali per cartella e hai un report di copertura.
  3. 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.
  4. 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.
  5. Sostituisci le API incompatibili una alla volta. watch diventa useWatch. 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 resta none, 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 useMemo solo dove un valore alimenta un effect. La memoizzazione esistente resta finché un test non dimostra che si può togliere, e il lint preserve-manual-memoization verifica che le sue dipendenze siano complete.
  • Fissa l'impostazione di Next.js. Scrivi enablePreserveExistingMemoizationGuarantees nella 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

Preferisci che lo facciamo noi?

Progettiamo e sviluppiamo questo tipo di lavoro per i clienti. Raccontaci di cosa hai bisogno.