Cours JavaScript et React · Module 10: Écosystème et avenir de React

React Compiler (React Forget)

8 min de lecture
Dans cette leçon8

Imagine que, sur ton vaisseau spatial, au lieu de configurer manuellement chaque système d'optimisation, tu disposes d'un pilote automatique intelligent qui détecte et optimise automatiquement toutes les opérations redondantes. C'est exactement ce que fait le React Compiler (aussi appelé React Forget) - il analyse automatiquement ton code et ajoute la mémoïsation là où elle est nécessaire. La version 1.0 est stable depuis octobre 2025 et fonctionne avec React 17, 18 et 19, au mieux avec la 19.

Le problème : la mémoïsation manuelle

Jusqu'à présent, l'optimisation des performances en React nécessitait l'utilisation manuelle de React.memo, useMemo et useCallback. C'est comme configurer manuellement chaque système d'un vaisseau spatial - chronophage, source d'erreurs et facile à oublier :

1// AVANT le React Compiler - mémoïsation manuelle
2import { useState, useMemo, useCallback, memo } from 'react';
3
4const MissionCard = memo(function MissionCard({ mission, onSelect }) {
5  return (
6    <div onClick={() => onSelect(mission.id)}>
7      <h3>{mission.name}</h3>
8      <p>Status: {mission.status}</p>
9    </div>
10  );
11});
12
13function MissionDashboard({ missions }) {
14  const [filter, setFilter] = useState('all');
15  const [selectedId, setSelectedId] = useState(null);
16
17  // Il faut penser à useMemo...
18  const filteredMissions = useMemo(
19    () => missions.filter(m => filter === 'all' || m.status === filter),
20    [missions, filter]
21  );
22
23  // ...et à useCallback pour chaque fonction passée en prop
24  const handleSelect = useCallback((id) => {
25    setSelectedId(id);
26  }, []);
27
28  return (
29    <div>
30      {filteredMissions.map(m => (
31        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
32      ))}
33    </div>
34  );
35}

Problèmes de la mémoïsation manuelle :

  • Facile à oublier - oublie un useCallback et React.memo ne fonctionnera pas
  • Code répétitif - beaucoup de code sans rapport avec la logique métier
  • Mauvaises dépendances - un tableau de dépendances incorrect dans useMemo/useCallback = bug
  • Sur-mémoïsation - mémoïser des choses qui n'ont pas besoin d'être optimisées

React Compiler - Mémoïsation automatique

Le React Compiler est un outil qui s'exécute au moment du build et qui analyse automatiquement les composants React pour ajouter la mémoïsation appropriée. Il agit au moment de la compilation - ton code source reste propre :

1// APRÈS le React Compiler - tu écris du code simple et propre
2function MissionDashboard({ missions }) {
3  const [filter, setFilter] = useState('all');
4  const [selectedId, setSelectedId] = useState(null);
5
6  // Le compilateur mémoïse AUTOMATIQUEMENT ce calcul
7  const filteredMissions = missions.filter(
8    m => filter === 'all' || m.status === filter
9  );
10
11  // Le compilateur stabilise AUTOMATIQUEMENT cette fonction
12  const handleSelect = (id) => {
13    setSelectedId(id);
14  };
15
16  return (
17    <div>
18      {filteredMissions.map(m => (
19        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
20      ))}
21    </div>
22  );
23}
24
25// Pas besoin de React.memo - le compilateur s'en charge !
26function MissionCard({ mission, onSelect }) {
27  return (
28    <div onClick={() => onSelect(mission.id)}>
29      <h3>{mission.name}</h3>
30      <p>Status: {mission.status}</p>
31    </div>
32  );
33}

Comment fonctionne le React Compiler ?

Le Compiler analyse ton code pendant le build (par exemple avec un plugin Babel) et génère une version optimisée :

1// Ton code source (ce que tu écris) :
2function StarList({ stars, highlight }) {
3  const visible = stars.filter(s => s.magnitude < 6);
4  const style = { color: highlight ? '#ffd700' : '#ccc' };
5
6  return (
7    <ul style={style}>
8      {visible.map(s => <li key={s.id}>{s.name}</li>)}
9    </ul>
10  );
11}
12
13// Ce que génère le compilateur (exemple simplifié) :
14function StarList_compiled({ stars, highlight }) {
15  // Le compilateur ajoute un cache pour le filtrage
16  const visible = useMemo(
17    () => stars.filter(s => s.magnitude < 6),
18    [stars]
19  );
20
21  // Le compilateur met en cache l'objet style
22  const style = useMemo(
23    () => ({ color: highlight ? '#ffd700' : '#ccc' }),
24    [highlight]
25  );
26
27  return (
28    <ul style={style}>
29      {visible.map(s => <li key={s.id}>{s.name}</li>)}
30    </ul>
31  );
32}

Le Compiler fait cela intelligemment - il ne mémoïse pas tout, seulement les parties qui peuvent réellement se répéter avec les mêmes données.

Configurer le React Compiler

Installation avec Babel

Le compilateur est le plugin Babel babel-plugin-react-compiler, que tu installes comme dépendance de développement. Dans la configuration, il doit venir en premier dans la liste des plugins, car il a besoin du code d'origine avant que d'autres transformations ne le modifient :

1npm install -D babel-plugin-react-compiler@latest
1// babel.config.js
2module.exports = {
3  plugins: [
4    // Le React Compiler doit s'exécuter en premier
5    ['babel-plugin-react-compiler', {
6      compilationMode: 'infer', // mode par défaut
7      panicThreshold: 'none', // un composant en erreur reste sans optimisation, et le build continue
8    }],
9    // ...autres plugins
10  ],
11};

Dans un projet sous React 17 ou 18, ajoute l'option target: '18' (ou '17') et installe le paquet react-compiler-runtime, car le code compilé a alors besoin de cette bibliothèque. Dans Vite 8 avec @vitejs/plugin-react 6, tu ajoutes le compilateur via @rolldown/plugin-babel et le preset reactCompilerPreset :

1// vite.config.js
2import { defineConfig } from 'vite';
3import react, { reactCompilerPreset } from '@vitejs/plugin-react';
4import babel from '@rolldown/plugin-babel';
5
6export default defineConfig({
7  plugins: [
8    react(),
9    babel({
10      presets: [reactCompilerPreset()]
11    }),
12  ],
13});

Installation avec Next.js

Dans Next.js 16, tu installes le même plugin comme dépendance de développement et tu actives l'option reactCompiler au premier niveau de la configuration. Dans les versions précédentes de Next.js, elle se trouvait dans la section experimental. Next.js n'exécute le plugin que pour les fichiers contenant du JSX ou des hooks, donc le build ne ralentit que légèrement :

1// next.config.js
2const nextConfig = {
3  reactCompiler: true,
4};
5
6module.exports = nextConfig;

Tu peux aussi activer le compilateur progressivement : reactCompiler: { compilationMode: 'annotation' } ne compile que les composants dotés de la directive 'use memo' au début de la fonction, et la directive 'use no memo' désactive la compilation d'un composant choisi.

Modes de compilation

  • infer (par défaut) - compile les fonctions nommées comme des composants (PascalCase) ou des hooks (use...) qui créent du JSX ou appellent d'autres hooks, ainsi que les fonctions dotées de la directive 'use memo'
  • annotation - ne compile que les fonctions marquées avec la directive 'use memo', ce qui facilite une adoption progressive
  • all - compile toutes les fonctions de premier niveau, y compris celles qui ne sont pas des composants, c'est pourquoi ce mode n'est pas recommandé

Les Rules of React

Le React Compiler ne fonctionne correctement QUE si ton code respecte les « Rules of React » :

1// BIEN - fonction pure, sans effets de bord pendant le rendu
2function GoodComponent({ data }) {
3  const sorted = [...data].sort((a, b) => a.name.localeCompare(b.name));
4  return <ul>{sorted.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
5}
6
7// MAL - mutation des props (le compilateur peut générer du code incorrect !)
8function BadComponent({ data }) {
9  data.sort((a, b) => a.name.localeCompare(b.name)); // Mutation !
10  return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
11}
12
13// MAL - effet de bord pendant le rendu
14function BadComponent2({ userId }) {
15  // Le compilateur supposera que c'est pur et pourra l'ignorer !
16  analytics.track('render', userId);
17  return <div>User: {userId}</div>;
18}

Règles clés :

  1. Les composants doivent être des fonctions pures - même entrée = même sortie
  2. Ne mute pas les props ni l'état directement
  3. Les effets de bord uniquement dans useEffect, les gestionnaires d'événements ou les callbacks
  4. Ne lis pas les refs pendant le rendu (uniquement dans les effets et les gestionnaires)

Plugin ESLint

Les règles du compilateur font désormais partie du paquet officiel eslint-plugin-react-hooks (depuis la version 6, actuellement 7.x), à côté de rules-of-hooks et exhaustive-deps. Tu l'installes avec npm install -D eslint-plugin-react-hooks@latest. Le preset recommended en configuration flat active toutes les règles recommandées, et tu règles le niveau d'une règle précise dans la section rules :

1// eslint.config.js
2import reactHooks from 'eslint-plugin-react-hooks';
3import { defineConfig } from 'eslint/config';
4
5export default defineConfig([
6  {
7    // fichiers .js et .jsx avec le parseur JSX (sans cela, ESLint 9 ignore les fichiers .jsx)
8    files: ['**/*.{js,jsx}'],
9    languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
10  },
11  reactHooks.configs.flat.recommended,
12  {
13    rules: {
14      'react-hooks/purity': 'error', // rendu sans effets de bord, par exemple sans Math.random()
15      'react-hooks/refs': 'error', // ne pas lire ni écrire ref.current pendant le rendu
16      'react-hooks/immutability': 'error', // ne pas muter les props ni l'état
17    },
18  },
19]);

L'ancien paquet eslint-plugin-react-compiler avec la règle 'react-compiler/react-compiler' a été remplacé par ces règles, donc ne l'ajoute pas aux nouveaux projets. Le linter montre aussi quels composants le compilateur ignore et pourquoi.

Migrer du code existant

Si tu as un projet existant avec de la mémoïsation manuelle, tu peux adopter le compilateur progressivement, par exemple en mode annotation. Les React.memo, useMemo et useCallback existants n'entrent pas en conflit avec le compilateur. La documentation de React recommande de les laisser en place ou de ne les retirer qu'après des tests, car leur suppression peut changer le résultat de la compilation. Dans le nouveau code, appuie-toi sur le compilateur et garde useMemo et useCallback comme porte de sortie quand tu as besoin d'un contrôle précis, par exemple sur une dépendance d'effet.

Récapitulatif

Le React Compiler 1.0 change la façon d'optimiser React :

  1. Mémoïsation automatique - le Compiler mémorise les résultats à ta place, comme si tu avais ajouté useMemo, useCallback et React.memo là où il faut
  2. Code plus propre - tu écris un code plus simple et plus facile à maintenir
  3. Moins de bugs - le Compiler n'oubliera pas le tableau de dépendances
  4. Au moment du build - les optimisations sont créées à la compilation, et il ne reste à l'exécution qu'un cache léger des résultats
  5. Nécessite un code propre - les composants doivent respecter les Rules of React

Souviens-toi : le React Compiler est comme un pilote automatique sur un vaisseau spatial - il fait son travail à la perfection, mais exige que le vaisseau soit correctement construit.

Code de cette leçon : App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// === REACT COMPILER - Simulation de la mémoïsation automatique ===
4// Comparaison : mémoïsation manuelle et code « après compilation »
5
6// Compteur de rendus : un effet de bord volontaire dans le rendu, uniquement pour la mesure.
7// Dans une vraie appli, c’est le Profiler des React DevTools qui mesure les rendus.
8let renderLog = {};
9function trackRender(name) {
10  renderLog[name] = (renderLog[name] || 0) + 1;
11  return renderLog[name];
12}
13
14// === VERSION 1 : SANS mémoïsation (code naïf) ===
15function NaiveMissionCard({ mission, onSelect }) {
16  const count = trackRender('Naive-' + mission.id);
17  return (
18    <div style={{ ...styles.card, borderColor: 'rgba(255,107,107,0.3)' }}>
19      <div style={styles.renderBadge('#ff6b6b')}>R: {count}</div>
20      <strong>{mission.name}</strong>
21      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
22      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Sélectionner</button>
23    </div>
24  );
25}
26
27// === VERSION 2 : mémoïsation MANUELLE (React.memo + useCallback + useMemo) ===
28const ManualMissionCard = memo(function ManualMissionCard({ mission, onSelect }) {
29  const count = trackRender('Manual-' + mission.id);
30  return (
31    <div style={{ ...styles.card, borderColor: 'rgba(0,212,255,0.3)' }}>
32      <div style={styles.renderBadge('#00d4ff')}>R: {count}</div>
33      <strong>{mission.name}</strong>
34      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
35      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Sélectionner</button>
36    </div>
37  );
38});
39
40// === VERSION 3 : « compilée » (simulation du React Compiler) ===
41// Dans un vrai projet, le compilateur générerait cette mémoïsation
42const CompiledMissionCard = memo(function CompiledMissionCard({ mission, onSelect }) {
43  const count = trackRender('Compiled-' + mission.id);
44  return (
45    <div style={{ ...styles.card, borderColor: 'rgba(0,255,136,0.3)' }}>
46      <div style={styles.renderBadge('#00ff88')}>R: {count}</div>
47      <strong>{mission.name}</strong>
48      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
49      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Sélectionner</button>
50    </div>
51  );
52});
53
54const STATUS_LABELS = { active: 'active', planned: 'planifiée', completed: 'terminée' };
55
56// Données de test
57const MISSIONS = [
58  { id: 1, name: 'Alpha Centauri', status: 'active' },
59  { id: 2, name: 'Europa Probe', status: 'planned' },
60  { id: 3, name: 'Mars Colony', status: 'completed' },
61  { id: 4, name: 'Titan Explorer', status: 'active' },
62];
63
64function App() {
65  const [counter, setCounter] = useState(0);
66  const [selected, setSelected] = useState(null);
67  const [filter, setFilter] = useState('all');
68
69  // NAÏF : nouvelle fonction et filtrage à chaque rendu
70  const naiveFiltered = MISSIONS.filter(m => filter === 'all' || m.status === filter);
71  const naiveSelect = (id) => setSelected(id);
72
73  // MANUEL : useMemo + useCallback
74  const manualFiltered = useMemo(
75    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
76    [filter]
77  );
78  const manualSelect = useCallback((id) => setSelected(id), []);
79
80  // « COMPILÉ » : identique au manuel, mais ajouté automatiquement par le compilateur
81  const compiledFiltered = useMemo(
82    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
83    [filter]
84  );
85  const compiledSelect = useCallback((id) => setSelected(id), []);
86
87  return (
88    <div style={styles.container}>
89      <header style={styles.header}>
90        <h1 style={styles.title}>React Compiler - comparaison</h1>
91        <p style={styles.subtitle}>Clique sur « Re-render » et observe les compteurs de rendus</p>
92      </header>
93
94      <div style={styles.controls}>
95        <select value={filter} onChange={e => setFilter(e.target.value)} style={styles.select}>
96          <option value="all">Toutes</option>
97          <option value="active">Actives</option>
98          <option value="planned">Planifiées</option>
99          <option value="completed">Terminées</option>
100        </select>
101        <button onClick={() => setCounter(c => c + 1)} style={styles.reRenderBtn}>
102          Re-render ({counter})
103        </button>
104        {selected && <span style={styles.selectedInfo}>ID sélectionné : {selected}</span>}
105      </div>
106
107      <div style={styles.grid}>
108        <div style={styles.column}>
109          <h3 style={{ ...styles.colTitle, color: '#ff6b6b' }}>Sans mémoïsation</h3>
110          <p style={styles.colDesc}>Chaque re-render = nouvelle fonction + nouveau filtrage</p>
111          {naiveFiltered.map(m => (
112            <NaiveMissionCard key={m.id} mission={m} onSelect={naiveSelect} />
113          ))}
114        </div>
115
116        <div style={styles.column}>
117          <h3 style={{ ...styles.colTitle, color: '#00d4ff' }}>Mémoïsation manuelle</h3>
118          <p style={styles.colDesc}>React.memo + useCallback + useMemo</p>
119          {manualFiltered.map(m => (
120            <ManualMissionCard key={m.id} mission={m} onSelect={manualSelect} />
121          ))}
122        </div>
123
124        <div style={styles.column}>
125          <h3 style={{ ...styles.colTitle, color: '#00ff88' }}>React Compiler</h3>
126          <p style={styles.colDesc}>Mémoïsation ajoutée automatiquement</p>
127          {compiledFiltered.map(m => (
128            <CompiledMissionCard key={m.id} mission={m} onSelect={compiledSelect} />
129          ))}
130        </div>
131      </div>
132    </div>
133  );
134}
135
136const styles = {
137  container: { fontFamily: 'system-ui', background: '#0a0e17', color: '#e0e1dd', minHeight: '100vh', padding: '14px' },
138  header: { textAlign: 'center', marginBottom: '12px' },
139  title: { fontSize: '20px', color: '#00d4ff', margin: '0 0 4px' },
140  subtitle: { fontSize: '12px', color: '#8892b0', margin: 0 },
141  controls: { display: 'flex', gap: '8px', justifyContent: 'center', alignItems: 'center', marginBottom: '14px', flexWrap: 'wrap' },
142  select: { padding: '5px 10px', background: '#1a1f2e', color: '#e0e1dd', border: '1px solid rgba(0,212,255,0.3)', borderRadius: '4px', fontSize: '12px' },
143  reRenderBtn: { padding: '5px 14px', background: 'rgba(255,107,107,0.15)', color: '#ff6b6b', border: '1px solid rgba(255,107,107,0.3)', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' },
144  selectedInfo: { fontSize: '12px', color: '#ffd700' },
145  grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '10px' },
146  column: { background: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '10px' },
147  colTitle: { fontSize: '13px', margin: '0 0 4px' },
148  colDesc: { fontSize: '10px', color: '#556070', margin: '0 0 8px' },
149  card: { display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 8px', background: 'rgba(0,0,0,0.3)', borderRadius: '4px', marginBottom: '4px', fontSize: '11px', border: '1px solid', position: 'relative' },
150  renderBadge: (color) => ({ position: 'absolute', top: '2px', right: '4px', fontSize: '9px', color, fontWeight: 'bold' }),
151  status: (s) => ({ padding: '1px 5px', borderRadius: '6px', fontSize: '9px', background: s === 'active' ? 'rgba(0,255,136,0.15)' : s === 'completed' ? 'rgba(0,212,255,0.15)' : 'rgba(255,170,0,0.15)', color: s === 'active' ? '#00ff88' : s === 'completed' ? '#00d4ff' : '#ffaa00' }),
152  selectBtn: { marginLeft: 'auto', padding: '2px 6px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '3px', color: '#8892b0', cursor: 'pointer', fontSize: '10px' },
153};
154
155export default App;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que fait automatiquement le compilateur React lors de la compilation ?

  2. 2. Quelle pratique ENFREINT les règles de React et peut provoquer des erreurs dans le compilateur React ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Compose la configuration du compilateur React dans next.config.js (Next.js 16) :

  • Tri horizontal

    Compose la règle ESLint qui vérifie la pureté des composants - une des règles du compilateur React dans eslint-plugin-react-hooks :

Articles utiles