Cours JavaScript et React · Module 6: React Router
Le chargement différé des composants pendant un voyage cosmique
Dans cette leçon8
Pendant un long voyage intergalactique, nous n'avons pas besoin de charger immédiatement tout l'équipement à bord du vaisseau. De même dans les applications React, en particulier les plus grandes, nous n'avons pas besoin de charger tous les composants tout de suite lors de l'initialisation de l'application. C'est là que le chargement différé (lazy loading) vient à notre secours.
Qu'est-ce que le chargement différé ?
Le chargement différé est une technique d'optimisation des performances qui consiste à charger des composants, des modules ou des ressources uniquement lorsqu'ils sont réellement nécessaires, au lieu de les charger d'emblée lors de l'initialisation de l'application. Dans React, nous l'accomplissons à l'aide de la fonction React.lazy() combinée à Suspense.
Les bases du chargement différé dans React
Pour implémenter le chargement différé d'un composant, au lieu de l'import standard du composant :
1import HeavyDashboard from './HeavyDashboard';Nous utilisons la fonction React.lazy() avec un import dynamique :
1import React, { lazy, Suspense } from 'react';
2
3// Chargement différé d'un composant
4const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
5
6function App() {
7 return (
8 <div>
9 <h1>Centre de contrôle de vol</h1>
10
11 {/* Suspense fournit un composant de remplacement pendant le chargement */}
12 <Suspense fallback={<div>Chargement du panneau de contrôle...</div>}>
13 <HeavyDashboard />
14 </Suspense>
15 </div>
16 );
17}Dans cet exemple :
lazy()accepte une fonction qui doit appeler unimport()dynamiqueSuspenseaffiche un fallback (composant de remplacement) pendant le chargement du composant différé- Le composant
HeavyDashboardréel ne se chargera que lorsqu'il sera rendu
Le chargement différé dans React Router
Dans les applications utilisant React Router, nous pouvons charger de façon différée les composants de chaque route. Ainsi, le bundle initial de l'application sera plus petit, et l'utilisateur ne téléchargera le code que pour les parties de l'application qu'il visite :
1import React, { lazy, Suspense } from 'react';
2import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
4// Composants chargés de façon différée
5const MissionControl = lazy(() => import('./MissionControl'));
6const SpaceshipSettings = lazy(() => import('./SpaceshipSettings'));
7const GalacticMap = lazy(() => import('./GalacticMap'));
8const CrewQuarters = lazy(() => import('./CrewQuarters'));
9
10// Composant toujours chargé (petit et fréquemment utilisé)
11import NavigationBar from './NavigationBar';
12
13function App() {
14 return (
15 <Router>
16 <NavigationBar />
17
18 <Suspense fallback={<div className="loading-screen">Préparation du module...</div>}>
19 <Routes>
20 <Route path="/" element={<HomePage />} />
21 <Route path="/mission-control" element={<MissionControl />} />
22 <Route path="/spaceship-settings" element={<SpaceshipSettings />} />
23 <Route path="/galactic-map" element={<GalacticMap />} />
24 <Route path="/crew-quarters" element={<CrewQuarters />} />
25 </Routes>
26 </Suspense>
27 </Router>
28 );
29}Dans cet exemple, seuls les composants de la route actuellement visitée seront chargés. Nous pouvons aussi ajouter des composants Suspense individuels pour chaque route si nous voulons différents indicateurs de chargement :
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route
4 path="/mission-control"
5 element={
6 <Suspense fallback={<div>Initialisation du panneau de contrôle de mission...</div>}>
7 <MissionControl />
8 </Suspense>
9 }
10 />
11 <Route
12 path="/galactic-map"
13 element={
14 <Suspense fallback={<div>Chargement de la carte galactique...</div>}>
15 <GalacticMap />
16 </Suspense>
17 }
18 />
19</Routes>Techniques avancées de chargement différé
Prefetching (pré-chargement)
Parfois, nous savons avec une forte probabilité que l'utilisateur visitera une route spécifique. Nous pouvons alors pré-charger le composant avant même que l'utilisateur ne clique sur le lien :
1import { lazy } from 'react';
2
3// Déclaration du composant différé
4const GalacticMap = lazy(() => import('./GalacticMap'));
5
6// Composant de navigation avec prefetching
7function NavigationLink({ to, children }) {
8 // Fonction de pré-chargement
9 const prefetchComponent = () => {
10 // Import dynamique sous forme de Promise - démarre le chargement
11 import('./GalacticMap');
12 };
13
14 return (
15 <Link
16 to={to}
17 onMouseOver={prefetchComponent} // Démarrer le chargement au survol de la souris
18 onFocus={prefetchComponent} // Pour l'accessibilité, chargement au focus
19 >
20 {children}
21 </Link>
22 );
23}
24
25// Utilisation dans un composant
26function Navigation() {
27 return (
28 <nav>
29 <NavigationLink to="/galactic-map">Carte galactique</NavigationLink>
30 </nav>
31 );
32}Regroupement de routes (route-based code splitting)
Nous pouvons regrouper des composants liés chargés de façon différée pour réduire le nombre de requêtes réseau distinctes :
1// Regroupement des composants liés au cockpit
2const CockpitModule = lazy(() => import('./modules/CockpitModule'));
3
4// Utilisation du module regroupé dans le routage
5<Route path="/cockpit/*" element={<CockpitModule />} />
6
7// Dans CockpitModule.js, nous avons des routes internes
8function CockpitModule() {
9 return (
10 <Routes>
11 <Route path="/" element={<CockpitOverview />} />
12 <Route path="/navigation" element={<NavigationControls />} />
13 <Route path="/engines" element={<EngineControls />} />
14 <Route path="/life-support" element={<LifeSupportSystems />} />
15 </Routes>
16 );
17}Avec cette approche, tous les composants du cockpit seront récupérés en un seul morceau de code lorsque l'utilisateur entre dans n'importe quelle route /cockpit/*.
Gestion des erreurs pendant le chargement
Dans l'espace, nous devons toujours être prêts à des situations imprévues. Dans le cas du chargement différé, il peut arriver que la tentative de chargement d'un composant échoue (par ex. à cause de problèmes de réseau) :
1import React, { lazy, Suspense } from 'react';
2import { ErrorBoundary } from 'react-error-boundary';
3
4const GalacticMap = lazy(() => import('./GalacticMap'));
5
6function ErrorFallback({ error, resetErrorBoundary }) {
7 return (
8 <div className="error-container">
9 <h2>Problème rencontré lors du chargement du module</h2>
10 <p>Erreur : {error.message}</p>
11 <button onClick={resetErrorBoundary}>
12 Réessayer
13 </button>
14 </div>
15 );
16}
17
18function MapSection() {
19 return (
20 <ErrorBoundary
21 FallbackComponent={ErrorFallback}
22 onReset={() => {
23 // Logique optionnelle de réinitialisation de l'état
24 }}
25 >
26 <Suspense fallback={<div>Chargement de la carte galactique...</div>}>
27 <GalacticMap />
28 </Suspense>
29 </ErrorBoundary>
30 );
31}Quand utiliser le chargement différé ?
Le chargement différé est particulièrement utile dans les cas suivants :
Grandes applications comportant de nombreux composants, où l'utilisateur n'utilise généralement qu'une petite partie des fonctionnalités au cours d'une session
Fonctionnalités rarement utilisées - par exemple, un panneau d'administration utilisé seulement par quelques utilisateurs
Dépendances lourdes - composants utilisant de grandes bibliothèques (par ex. éditeurs de texte, cartes, graphiques)
Parcours utilisateur alternatifs - lorsque différents utilisateurs utilisent différentes parties de l'application selon leur rôle
Bonnes pratiques
1. Place Suspense plus haut dans l'arbre des composants
Au lieu d'envelopper chaque composant chargé de façon différée dans un Suspense distinct, il est souvent préférable de placer un seul Suspense plus haut dans l'arbre :
1function App() {
2 return (
3 <Suspense fallback={<GlobalLoadingSpinner />}>
4 <Router>
5 <Routes>
6 {/* Routes chargées de façon différée */}
7 </Routes>
8 </Router>
9 </Suspense>
10 );
11}2. Applique des limites de découpage de code appropriées
Découpe le code selon les frontières naturelles de l'application :
- Découpage par routes (routing)
- Découpage par modules fonctionnels
- Découpage entre les parties publiques et privées de l'application
3. Évite un découpage trop fin
Un découpage de code trop fin peut conduire à de nombreuses petites requêtes réseau, ce qui peut être moins efficace que quelques requêtes plus volumineuses. Trouve le juste milieu.
4. Fournis toujours un bon fallback
Assure-toi que le fallback dans Suspense est visuellement cohérent avec le reste de l'application et donne à l'utilisateur une information claire sur l'état de chargement.
5. Teste sur de vrais appareils et des connexions lentes
Le chargement différé peut très bien fonctionner sur les connexions rapides des développeurs, mais teste toujours l'expérience utilisateur sur des connexions plus lentes.
Exemple d'implémentation
Voici un exemple d'application spatiale plus étendue avec chargement différé :
1import React, { lazy, Suspense } from 'react';
2import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3import { ErrorBoundary } from 'react-error-boundary';
4
5// Composants toujours chargés
6import MainLayout from './layouts/MainLayout';
7import LoadingScreen from './components/LoadingScreen';
8import ErrorFallback from './components/ErrorFallback';
9
10// Pages chargées de façon différée
11const HomePage = lazy(() => import('./pages/HomePage'));
12const Dashboard = lazy(() => import('./pages/Dashboard'));
13const NavigationModule = lazy(() => import('./modules/NavigationModule'));
14const LifeSupportModule = lazy(() => import('./modules/LifeSupportModule'));
15const CommunicationModule = lazy(() => import('./modules/CommunicationModule'));
16const CrewModule = lazy(() => import('./modules/CrewModule'));
17const SettingsPage = lazy(() => import('./pages/SettingsPage'));
18const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
19
20function App() {
21 return (
22 <Router>
23 <ErrorBoundary FallbackComponent={ErrorFallback}>
24 <MainLayout>
25 <Suspense fallback={<LoadingScreen />}>
26 <Routes>
27 <Route path="/" element={<HomePage />} />
28 <Route path="/dashboard" element={<Dashboard />} />
29
30 {/* Modules imbriqués avec leurs propres routes */}
31 <Route path="/navigation/*" element={<NavigationModule />} />
32 <Route path="/life-support/*" element={<LifeSupportModule />} />
33 <Route path="/communication/*" element={<CommunicationModule />} />
34 <Route path="/crew/*" element={<CrewModule />} />
35
36 <Route path="/settings" element={<SettingsPage />} />
37 <Route path="/404" element={<NotFoundPage />} />
38 <Route path="*" element={<Navigate to="/404" replace />} />
39 </Routes>
40 </Suspense>
41 </MainLayout>
42 </ErrorBoundary>
43 </Router>
44 );
45}
46
47export default App;Résumé
Le chargement différé est une technique puissante d'optimisation des applications React qui permet de :
- Réduire la taille du bundle JavaScript initial
- Raccourcir le temps de chargement de l'application
- Améliorer l'expérience utilisateur grâce à un rendu initial plus rapide
- Charger le code uniquement lorsqu'il est réellement nécessaire
C'est comme une gestion intelligente des ressources à bord d'un vaisseau spatial - au lieu de tout emporter au début du voyage, nous ne transportons que ce qui est nécessaire sur le moment, économisant du carburant et accélérant le décollage.
Code de cette leçon : App.jsx
1import React from 'react';
2import { BrowserRouter, Routes, Route, Link, useSearchParams } from 'react-router-dom';
3
4// Liste de produits avec filtrage
5function ProductList() {
6 const [searchParams, setSearchParams] = useSearchParams();
7
8 const category = searchParams.get('category') || 'all';
9 const sortBy = searchParams.get('sort') || 'name';
10 const search = searchParams.get('search') || '';
11
12 const products = [
13 { id: 1, name: 'Combinaison spatiale', category: 'equipment', price: 5000 },
14 { id: 2, name: 'Casque spatial', category: 'equipment', price: 1500 },
15 { id: 3, name: 'Fusée miniature', category: 'toys', price: 200 },
16 { id: 4, name: 'Télescope', category: 'tools', price: 3000 },
17 { id: 5, name: 'Carte de la galaxie', category: 'tools', price: 500 },
18 { id: 6, name: 'Figurine spatiale', category: 'toys', price: 50 },
19 ];
20
21 // Filtrage
22 let filteredProducts = products;
23 if (category !== 'all') {
24 filteredProducts = filteredProducts.filter(p => p.category === category);
25 }
26 if (search) {
27 filteredProducts = filteredProducts.filter(p =>
28 p.name.toLowerCase().includes(search.toLowerCase())
29 );
30 }
31
32 // Tri
33 filteredProducts.sort((a, b) => {
34 if (sortBy === 'price') return a.price - b.price;
35 return a.name.localeCompare(b.name);
36 });
37
38 const updateFilter = (key, value) => {
39 const newParams = new URLSearchParams(searchParams);
40 if (value) {
41 newParams.set(key, value);
42 } else {
43 newParams.delete(key);
44 }
45 setSearchParams(newParams);
46 };
47
48 return (
49 <div className="product-list">
50 <h1>Boutique cosmique</h1>
51
52 <div className="filters">
53 <div className="filter-group">
54 <label>Rechercher :</label>
55 <input
56 type="text"
57 value={search}
58 onChange={(e) => updateFilter('search', e.target.value)}
59 placeholder="Saisis un nom..."
60 />
61 </div>
62
63 <div className="filter-group">
64 <label>Catégorie :</label>
65 <select
66 value={category}
67 onChange={(e) => updateFilter('category', e.target.value)}
68 >
69 <option value="all">Toutes</option>
70 <option value="equipment">Équipement</option>
71 <option value="tools">Outils</option>
72 <option value="toys">Jouets</option>
73 </select>
74 </div>
75
76 <div className="filter-group">
77 <label>Trier :</label>
78 <select
79 value={sortBy}
80 onChange={(e) => updateFilter('sort', e.target.value)}
81 >
82 <option value="name">Nom</option>
83 <option value="price">Prix</option>
84 </select>
85 </div>
86 </div>
87
88 <div className="current-params">
89 <strong>Paramètres d'URL :</strong> {searchParams.toString() || '(aucun)'}
90 </div>
91
92 <div className="products">
93 {filteredProducts.map(product => (
94 <div key={product.id} className="product">
95 <h3>{product.name}</h3>
96 <p className="category">{product.category}</p>
97 <p className="price">{product.price} crédits</p>
98 </div>
99 ))}
100 {filteredProducts.length === 0 && (
101 <p className="no-results">Aucun produit ne correspond aux critères</p>
102 )}
103 </div>
104 </div>
105 );
106}
107
108function App() {
109 return (
110 <BrowserRouter>
111 <div className="app">
112 <nav>
113 <Link to="/products">Tous</Link>
114 <Link to="/products?category=equipment">Équipement</Link>
115 <Link to="/products?category=tools">Outils</Link>
116 <Link to="/products?category=toys&sort=price">Jouets (prix)</Link>
117 </nav>
118 <Routes>
119 <Route path="/" element={<ProductList />} />
120 <Route path="/products" element={<ProductList />} />
121 </Routes>
122 </div>
123 </BrowserRouter>
124 );
125}
126
127export 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. Que retourne la fonction React.lazy() dans React ?
2. Quel est le rôle du composant Suspense avec le chargement différé ?
Tâches pratiques dans le jeu
- Éditeur de code
Routes protégées
- Clique dans l'ordre
Ordonne la syntaxe d'une Route avec un paramètre dynamique :
- Éditeur de code
Chargement différé des composants
- Tri vertical
Ordonne les étapes du chargement différé d'un composant dans React :