Améliorez les performances de votre application web avec ce guide complet sur le code splitting frontend. Découvrez les stratégies basées sur les routes et les composants avec des exemples pratiques pour React, Vue et Angular.
Code Splitting Frontend : Une Analyse Approfondie des Stratégies Basées sur les Routes et les Composants
Dans le paysage numérique moderne, la première impression d'un utilisateur sur votre site web est souvent définie par une seule métrique : la vitesse. Une application qui se charge lentement peut entraîner des taux de rebond élevés, des utilisateurs frustrés et des pertes de revenus. À mesure que les applications frontend gagnent en complexité, la gestion de leur taille devient un défi crucial. Le comportement par défaut de la plupart des bundlers est de créer un unique fichier JavaScript monolithique contenant tout le code de votre application. Cela signifie qu'un utilisateur visitant votre page d'accueil pourrait également télécharger le code du tableau de bord d'administration, des paramètres de profil utilisateur et d'un processus de paiement qu'il n'utilisera peut-être jamais.
C'est là que le code splitting (ou division du code) entre en jeu. C'est une technique puissante qui vous permet de diviser votre gros bundle JavaScript en morceaux plus petits et gérables qui peuvent être chargés à la demande. En n'envoyant que le code dont l'utilisateur a besoin pour la vue initiale, vous pouvez améliorer considérablement les temps de chargement, l'expérience utilisateur et avoir un impact positif sur les métriques de performance critiques comme les Core Web Vitals de Google.
Ce guide complet explorera les deux principales stratégies de code splitting frontend : basée sur les routes et basée sur les composants. Nous approfondirons le pourquoi, le comment et le quand de chaque approche, avec des exemples pratiques et concrets utilisant des frameworks populaires comme React, Vue et Angular.
Le Problème : Le Bundle JavaScript Monolithique
Imaginez que vous faites vos valises pour un voyage à plusieurs destinations qui inclut des vacances à la plage, une randonnée en montagne et une conférence d'affaires formelle. L'approche monolithique, c'est comme essayer de fourrer votre maillot de bain, vos chaussures de randonnée et votre costume d'affaires dans une seule et énorme valise. Lorsque vous arrivez à la plage, vous devez trimballer cette valise géante, même si vous n'avez besoin que du maillot de bain. C'est lourd, inefficace et encombrant.
Un bundle JavaScript monolithique présente des problèmes similaires pour une application web :
- Temps de chargement initial excessif : Le navigateur doit télécharger, analyser et exécuter l'intégralité du code de l'application avant que l'utilisateur puisse voir ou interagir avec quoi que ce soit. Cela peut prendre plusieurs secondes sur des réseaux plus lents ou des appareils moins puissants.
- Bande passante gaspillée : Les utilisateurs téléchargent du code pour des fonctionnalités auxquelles ils n'accéderont peut-être jamais, consommant inutilement leurs forfaits de données. C'est particulièrement problématique pour les utilisateurs mobiles dans les régions où l'accès à Internet est coûteux ou limité.
- Faible efficacité du cache : Une petite modification d'une seule ligne de code dans une fonctionnalité invalide le cache de l'ensemble du bundle. L'utilisateur est alors obligé de retélécharger toute l'application, même si 99 % de celle-ci est inchangée.
- Impact négatif sur les Core Web Vitals : Les gros bundles nuisent directement à des métriques comme le Largest Contentful Paint (LCP) et le Time to Interactive (TTI), ce qui peut affecter le classement SEO de votre site et la satisfaction des utilisateurs.
Le code splitting est la solution à ce problème. C'est comme préparer trois sacs séparés et plus petits : un pour la plage, un pour la montagne et un pour la conférence. Vous ne transportez que ce dont vous avez besoin, quand vous en avez besoin.
La Solution : Qu'est-ce que le Code Splitting ?
Le code splitting est le processus de division du code de votre application en divers bundles ou "chunks" qui peuvent ensuite être chargés à la demande ou en parallèle. Au lieu d'un gros `app.js`, vous pourriez avoir `main.js`, `dashboard.chunk.js`, `profile.chunk.js`, etc.
Les outils de build modernes comme Webpack, Vite et Rollup ont rendu ce processus incroyablement accessible. Ils tirent parti de la syntaxe dynamique `import()`, une fonctionnalité du JavaScript moderne (ECMAScript), qui vous permet d'importer des modules de manière asynchrone. Lorsqu'un bundler voit `import()`, il crée automatiquement un chunk séparé pour ce module et ses dépendances.
Explorons les deux stratégies les plus courantes et efficaces pour mettre en œuvre le code splitting.
Stratégie 1 : Le Code Splitting Basé sur les Routes
La division basée sur les routes est la stratégie de code splitting la plus intuitive et la plus largement adoptée. La logique est simple : si un utilisateur est sur la page `/home`, il n'a pas besoin du code pour les pages `/dashboard` ou `/settings`. En divisant votre code le long des routes de votre application, vous vous assurez que les utilisateurs ne téléchargent que le code de la page qu'ils consultent actuellement.
Comment ça marche
Vous configurez le routeur de votre application pour charger dynamiquement le composant associé à une route spécifique. Lorsqu'un utilisateur navigue vers cette route pour la première fois, le routeur déclenche une requête réseau pour récupérer le chunk JavaScript correspondant. Une fois chargé, le composant est rendu, et le chunk est mis en cache par le navigateur pour les visites ultérieures.
Avantages de la division basée sur les routes
- Réduction significative du chargement initial : Le bundle initial ne contient que la logique de base de l'application et le code de la route par défaut (par exemple, la page d'accueil), le rendant beaucoup plus petit et plus rapide à charger.
- Facile à mettre en œuvre : La plupart des bibliothèques de routage modernes ont un support intégré pour le lazy loading (chargement paresseux), ce qui rend l'implémentation simple.
- Limites logiques claires : Les routes fournissent des points de séparation naturels et clairs pour votre code, ce qui facilite la compréhension des parties de votre application qui sont divisées.
Exemples d'implémentation
React avec React Router
React fournit deux utilitaires principaux pour cela : `React.lazy()` et `
Exemple de `App.js` utilisant React Router :
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Importer statiquement les composants toujours nécessaires
import Navbar from './components/Navbar';
import LoadingSpinner from './components/LoadingSpinner';
// Importer paresseusement les composants de route
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
function App() {
return (
<Router>
<Navbar />
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
Dans cet exemple, le code pour `DashboardPage` et `SettingsPage` ne sera pas inclus dans le bundle initial. Il ne sera récupéré du serveur que lorsqu'un utilisateur naviguera vers `/dashboard` ou `/settings` respectivement. Le composant `Suspense` assure une expérience utilisateur fluide en affichant un `LoadingSpinner` pendant cette récupération.
Vue avec Vue Router
Vue Router prend en charge le lazy loading des routes nativement en utilisant la syntaxe dynamique `import()` directement dans votre configuration de route.
Exemple de `router/index.js` utilisant Vue Router :
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue'; // Importé statiquement pour le chargement initial
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// Code-splitting au niveau de la route
// Ceci génère un chunk séparé (about.[hash].js) pour cette route
// qui est chargé paresseusement lorsque la route est visitée.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '../views/DashboardView.vue')
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
Ici, le composant pour les routes `/about` et `/dashboard` est défini comme une fonction qui retourne un import dynamique. Le bundler comprend cela et crée des chunks séparés. Le `/* webpackChunkName: "about" */` est un "commentaire magique" qui indique à Webpack de nommer le chunk résultant `about.js` au lieu d'un ID générique, ce qui peut être utile pour le débogage.
Angular avec le Router Angular
Le routeur d'Angular utilise la propriété `loadChildren` dans la configuration de la route pour permettre le chargement paresseux de modules entiers.
Exemple de `app-routing.module.ts` :
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component'; // Fait partie du bundle principal
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'products',
// Charger paresseusement le ProductsModule
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'admin',
// Charger paresseusement le AdminModule
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Dans cet exemple Angular, le code lié aux fonctionnalités `products` et `admin` est encapsulé dans leurs propres modules (`ProductsModule` et `AdminModule`). La syntaxe `loadChildren` demande au routeur Angular de ne récupérer et charger ces modules que lorsqu'un utilisateur navigue vers une URL commençant par `/products` ou `/admin`.
Stratégie 2 : Le Code Splitting Basé sur les Composants
Bien que la division basée sur les routes soit un excellent point de départ, vous pouvez pousser l'optimisation des performances encore plus loin avec la division basée sur les composants. Cette stratégie consiste à ne charger les composants que lorsqu'ils sont réellement nécessaires dans une vue donnée, souvent en réponse à une interaction de l'utilisateur.
Pensez aux composants qui ne sont pas immédiatement visibles ou qui sont utilisés rarement. Pourquoi leur code devrait-il faire partie du chargement initial de la page ?
Cas d'utilisation courants pour la division basée sur les composants
- Modales et boîtes de dialogue : Le code d'une modale complexe (par exemple, un éditeur de profil utilisateur) n'a besoin d'être chargé que lorsque l'utilisateur clique sur le bouton pour l'ouvrir.
- Contenu sous la ligne de flottaison : Pour une longue page d'accueil, les composants complexes qui se trouvent loin en bas de la page peuvent être chargés uniquement lorsque l'utilisateur défile près d'eux.
- Éléments d'interface utilisateur complexes : Les composants lourds comme les graphiques interactifs, les sélecteurs de date ou les éditeurs de texte riche peuvent être chargés paresseusement pour accélérer le rendu initial de la page sur laquelle ils se trouvent.
- Feature Flags ou tests A/B : Charger un composant uniquement si un feature flag spécifique est activé pour l'utilisateur.
- Interface utilisateur basée sur les rôles : Un composant spécifique à l'administrateur sur le tableau de bord ne devrait être chargé que pour les utilisateurs ayant un rôle 'admin'.
Exemples d'implémentation
React
Vous pouvez utiliser le même modèle `React.lazy` et `Suspense`, mais déclencher le rendu de manière conditionnelle en fonction de l'état de l'application.
Exemple d'une modale chargée paresseusement :
import React, { useState, Suspense, lazy } from 'react';
import LoadingSpinner from './components/LoadingSpinner';
// Importer paresseusement le composant de la modale
const EditProfileModal = lazy(() => import('./components/EditProfileModal'));
function UserProfilePage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
return (
<div>
<h1>Profil Utilisateur</h1>
<p>Quelques informations utilisateur ici...</p>
<button onClick={openModal}>Modifier le profil</button>
{/* Le composant de la modale et son code ne seront chargés que lorsque isModalOpen est vrai */}
{isModalOpen && (
<Suspense fallback={<LoadingSpinner />}>
<EditProfileModal onClose={closeModal} />
</Suspense>
)}
</div>
);
}
export default UserProfilePage;
Dans ce scénario, le chunk JavaScript pour `EditProfileModal.js` n'est demandé au serveur qu'après que l'utilisateur a cliqué sur le bouton "Modifier le profil" pour la première fois.
Vue
La fonction `defineAsyncComponent` de Vue est parfaite pour cela. Elle vous permet de créer un wrapper autour d'un composant qui ne sera chargé que lorsqu'il sera effectivement rendu.
Exemple d'un composant de graphique chargé paresseusement :
<template>
<div>
<h1>Tableau de Bord des Ventes</h1>
<button @click="showChart = true" v-if="!showChart">Afficher le Graphique des Ventes</button>
<!-- Le composant SalesChart ne sera chargé et rendu que lorsque showChart est vrai -->
<SalesChart v-if="showChart" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const showChart = ref(false);
// Définir un composant asynchrone. La lourde bibliothèque de graphiques sera dans son propre chunk.
const SalesChart = defineAsyncComponent(() =>
import('../components/SalesChart.vue')
);
</script>
Ici, le code du composant potentiellement lourd `SalesChart` (et ses dépendances, comme une bibliothèque de graphiques) est isolé. Il n'est téléchargé et monté que lorsque l'utilisateur le demande explicitement en cliquant sur le bouton.
Techniques et Patrons Avancés
Une fois que vous maîtrisez les bases de la division par route et par composant, vous pouvez employer des techniques plus avancées pour affiner davantage l'expérience utilisateur.
Pré-chargement (Preloading) et Pré-lecture (Prefetching) des Chunks
Attendre qu'un utilisateur clique sur un lien avant de récupérer le code de la route suivante peut introduire un petit délai. Nous pouvons être plus malins à ce sujet en chargeant le code à l'avance.
- Prefetching (Pré-lecture) : Cela indique au navigateur de récupérer une ressource pendant son temps d'inactivité car l'utilisateur pourrait en avoir besoin pour une navigation future. C'est un indice de faible priorité. Par exemple, une fois que l'utilisateur se connecte, vous pouvez pré-lire le code du tableau de bord, car il est très probable qu'il s'y rende ensuite.
- Preloading (Pré-chargement) : Cela indique au navigateur de récupérer une ressource avec une haute priorité car elle est nécessaire pour la page actuelle, mais sa découverte a été retardée (par exemple, une police définie profondément dans un fichier CSS). Dans le contexte du code splitting, vous pourriez pré-charger un chunk lorsqu'un utilisateur survole un lien, rendant la navigation quasi instantanée lorsqu'il clique.
Les bundlers comme Webpack et Vite vous permettent d'implémenter cela à l'aide de "commentaires magiques" :
// Prefetch : bon pour les pages suivantes probables
import(/* webpackPrefetch: true, webpackChunkName: "dashboard" */ './pages/DashboardPage');
// Preload : bon pour les prochaines interactions Ă haute confiance sur la page actuelle
const openModal = () => {
import(/* webpackPreload: true, webpackChunkName: "profile-modal" */ './components/ProfileModal');
// ... puis ouvrir la modale
}
Gérer les états de chargement et d'erreur
Le chargement de code sur un réseau est une opération asynchrone qui peut échouer. Une implémentation robuste doit en tenir compte.
- États de chargement : Fournissez toujours un retour à l'utilisateur pendant qu'un chunk est en cours de récupération. Cela évite que l'interface utilisateur ne semble pas réactive. Les squelettes (des interfaces utilisateur de substitution qui imitent la mise en page finale) sont souvent une meilleure expérience utilisateur que des spinners génériques. Le `
` de React facilite cela. Dans Vue et Angular, vous pouvez utiliser `v-if`/`ngIf` avec un drapeau de chargement. - États d'erreur : Et si l'utilisateur est sur un réseau instable et que le chunk JavaScript ne parvient pas à se charger ? Votre application ne devrait pas planter. Encapsulez vos composants chargés paresseusement dans un Error Boundary (en React) ou utilisez `.catch()` sur la promesse d'import dynamique pour gérer l'échec avec élégance. Vous pourriez afficher un message d'erreur et un bouton "Réessayer".
Exemple d'Error Boundary avec React :
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
return (
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div>
<p>Oups ! Échec du chargement du composant.</p>
<button onClick={resetErrorBoundary}>Réessayer</button>
</div>
)}
>
<Suspense fallback={<Spinner />}>
<MyLazyLoadedComponent />
</Suspense>
</ErrorBoundary>
);
}
Outillage et Analyse
Vous ne pouvez pas optimiser ce que vous ne pouvez pas mesurer. L'outillage frontend moderne fournit d'excellents utilitaires pour visualiser et analyser les bundles de votre application.
- Webpack Bundle Analyzer : Cet outil crée une visualisation en treemap de vos bundles de sortie. Il est inestimable pour identifier ce qui se trouve à l'intérieur de chaque chunk, repérer les dépendances volumineuses ou en double, et vérifier que votre stratégie de code splitting fonctionne comme prévu.
- Vite (Rollup Plugin Visualizer) : Les utilisateurs de Vite peuvent utiliser `rollup-plugin-visualizer` pour obtenir un graphique interactif similaire de la composition de leur bundle.
En analysant régulièrement vos bundles, vous pouvez identifier des opportunités d'optimisation supplémentaires. Par exemple, vous pourriez découvrir qu'une grande bibliothèque comme `moment.js` ou `lodash` est incluse dans plusieurs chunks. Ce pourrait être une occasion de la déplacer vers un chunk partagé `vendors` ou de trouver une alternative plus légère.
Bonnes Pratiques et Pièges Courants
Bien que puissant, le code splitting n'est pas une solution miracle. L'appliquer incorrectement peut parfois nuire aux performances.
- Ne pas sur-diviser : Créer trop de petits chunks peut être contre-productif. Chaque chunk nécessite une requête HTTP distincte, et le surcoût de ces requêtes peut l'emporter sur les avantages de fichiers plus petits, en particulier sur les réseaux mobiles à haute latence. Trouvez un équilibre. Commencez par les routes, puis divisez stratégiquement uniquement les composants les plus volumineux ou les moins utilisés.
- Analyser les parcours utilisateurs : Divisez votre code en fonction de la manière dont les utilisateurs naviguent réellement dans votre application. Si 95 % des utilisateurs passent de la page de connexion directement au tableau de bord, envisagez de pré-lire le code du tableau de bord sur la page de connexion.
- Regrouper les dépendances communes : La plupart des bundlers ont des stratégies (comme le `SplitChunksPlugin` de Webpack) pour créer automatiquement un chunk `vendors` partagé pour les bibliothèques utilisées sur plusieurs routes. Cela évite la duplication et améliore la mise en cache.
- Faire attention au Cumulative Layout Shift (CLS) : Lors du chargement de composants, assurez-vous que votre état de chargement (comme un squelette) occupe le même espace que le composant final. Sinon, le contenu de la page sautera lorsque le composant se chargera, entraînant un mauvais score CLS.
Conclusion : Un Web Plus Rapide pour Tous
Le code splitting n'est plus une technique avancée de niche ; c'est une exigence fondamentale pour la construction d'applications web modernes et performantes. En abandonnant un unique bundle monolithique et en adoptant le chargement à la demande, vous pouvez offrir une expérience significativement plus rapide et plus réactive à vos utilisateurs, quelles que soient les conditions de leur appareil ou de leur réseau.
Commencez par le code splitting basé sur les routes — c'est le fruit le plus facile à cueillir qui offre le plus grand gain de performance initial. Une fois cela en place, analysez votre application avec un analyseur de bundle et identifiez les candidats pour le code splitting basé sur les composants. Concentrez-vous sur les composants volumineux, interactifs ou rarement utilisés pour affiner davantage les performances de chargement de votre application.
En appliquant judicieusement ces stratégies, vous ne rendez pas seulement votre site web plus rapide ; vous rendez le web plus accessible et agréable pour un public mondial, un chunk à la fois.