Micro-frontends : Comment bien les utiliser ?


1 - Le principe des micro-frontends (ou MFE)
Tout le monde connaît aujourd’hui les micro-services. Les micro-frontends (MFE) en sont les équivalents côté frontend.
Dans cet article, je vais vous expliquer comment cela fonctionne, pourquoi les utiliser, quels sont leurs avantages et inconvénients, et surtout, comment bien les mettre en œuvre. Je terminerai par un retour d’expérience concret sur un projet en production.
Commençons par une réalité terrain : tout n’est jamais tout rose, et il faut souvent composer avec de l’existant.
Les MFE permettent d’intégrer plus facilement des applications existantes développées avec Angular, React ou Vue.js… dans un projet commun, comme un intranet ou un extranet.
Il devient également possible de mutualiser un framework donné et de l’utiliser dans plusieurs MFE affichés sur une même page ou sur différentes pages.
Souvent, on pense qu’il faut découper au maximum et créer une multitude de MFE, mais ce n’est pas toujours la meilleure stratégie (comme pour les microservices). Avant de tout fragmenter, il est crucial d’analyser les interactions entre modules, leur communication, et la complexité que cela peut engendrer.
Une approche simple et efficace :
Un MFE par page,
Un autre pour le menu,
Un pour l’avatar utilisateur,
Un pour le panier, etc.
Cela permet déjà un bon découpage fonctionnel.
On retrouve alors une certaine parenté avec le principe des SPA (Single Page Applications), à la différence près qu’avec les MFE, on peut réutiliser les mêmes bibliothèques ou composants entre les différentes pages, et cela grâce à notre meilleur allié : le navigateur (et son cache).
Voici une illustration de ce principe :
On comprend alors que ce qui s’applique au back peut également s’appliquer au front.
La plupart des entreprises ont déjà abandonné les monolithes au profit d’un découpage du backend en différents services, mais rares sont celles qui vont jusqu’à adopter ce principe sur la couche frontend (MFE)
Beaucoup d’entreprises préfèrent les applications distinctes connectées par SSO, ici l’idée est de tout unifier dans une seule application.
2 - MFE : de la théorie à la pratique
En théorie, le principe du MFE consiste à réunir sur une même page différents composants (ou applications) produits par différentes équipes, dans différents langages ou frameworks. Cela peut être représenté comme suit :
On retrouve donc une partie commune (et/ou partagée), tandis que chaque MFE est libre d’afficher ce qu’il souhaite, indépendamment des autres.
✅ Avantages
Migration progressive : chaque MFE peut évoluer indépendamment.
Suivi de projet facilité : meilleur reporting pour les managers.
Souplesse dans le recrutement : plus de choix technologiques.
Flexibilité : déploiement, modification ou désactivation à chaud. (cette partie sera détaillée plus tard)
Expérience utilisateur unifiée : login, droits, notifications, design...
❌ Inconvénients
Temps de chargement plus long sans optimisation.
Multiplicité technologique à encadrer sérieusement.
Harmonisation graphique nécessaire (design system, rigueur CSS…).
Comme pour les micro-services, la conception des MFE se fera dans une optique d’unification des technologies favorisant les capitalisations des équipes de développements.
Si dans une architecture micro-service, on décide d’utiliser un langage commun comme Java, on essayera de créer des modules communs pour la gestion des exceptions, des logs… et surtout il sera possible de capitaliser sur un framework commun comme SpringBoot. Le découpage permet une gestion des montées de version par service, en gérant les éventuelles dépendances; il apporte une autonomie technologique des services et des migrations possibles vers une autre techno comme Quarkus.
Pour le frontend, c'est le même principe, si on part sur Javascript et le framework Angular, on va pouvoir créer des librairies communes et les partager entre MFE… mais ensuite si la société fusionne avec une autre société qui est sous React, on pourra, dans un premier temps, unifier l’apparence en mode quick-win pour ensuite refondre un à un chaque MFE et opter pour l’un ou l’autre des frameworks.
Sur les grands projets complexes d’un point de vue métier et IHM, les migrations sont souvent très couteuses et sans valeur ajoutée métier.
Il sera donc plus facile de migrer chaque MFE un à un, plutôt que de refondre tout d’un coup. Le pattern micro-frontend facilite la migration des MFE les uns après les autres en évitant le big bang.
Peut-être que durant quelques mois, il y aura 2 versions à maintenir et donc 2 versions d’un framework à charger sur une ou plusieurs pages mais il sera possible de recetter les premiers MFE migrés sans affecter les autres. De plus, à tout moment, en fonction du budget, on pourra stopper la migration en attendant un moment plus opportun pour la terminer. Et ainsi appliquer le pattern du recouvrement de Martin Fowler qui permet d’envisager une réécriture opportuniste.
3 - MFE : la mise en place
Pour mettre en place des MFE, il y a plusieurs solutions, et selon moi, il y a 2 grandes tendances :
Module Federation (Webpack ou framework) : optimisation maximale, mais plus complexe.
Web Components natifs (Custom Elements) : approche simple et moderne.
La fédération va permettre de tout préparer avant de livrer et sera plus optimisée pour un site nécessitant de la haute performance et à forts enjeux commerciaux (billetterie en ligne, site d'enchères…)
La solution native va par contre faire l'inverse, on va déporter la charge sur chacun des navigateurs puisqu’un “Custom Element” est simplement un sélecteur HTML avec des liens vers des ressources javascript ou css.
C'est donc le navigateur qui va charger les fichiers JS et CSS et qui va les exécuter localement.
A la première visite, le navigateur devra charger (en asynchrone si possible) chaque fichier pour ensuite les interpréter localement, ici pas de SSR (Server Side Rendering) mais pour un intranet ou extranet, on ne cherche pas à être indexé par Google (voire on ne veut surtout pas, qui voudrait que Google indexe l'état de son compte en banque ou son compte Ameli ?)
C'est cette seconde solution que je voudrais vous présenter ici (la première solution est déjà traitée dans des dizaines d'articles de blogs). Cette seconde solution va justement plaire aux grandes entreprises qui préféreront déporter la charge CPU vers les millions de navigateurs plutôt que de la supporter sur les serveurs.
Au final, on va obtenir une page HTML (statique ou non) qui va contenir des éléments qui sont de simples liens vers des petits fichiers statiques qui seront mis en cache et chargés uniquement à la première visite, et si cet élément n'est pas mis à jour durant 6 mois, le navigateur pourra réutiliser les fichiers déjà présents dans son cache sans sollicitation au serveur.
Le serveur ne fournit que la page en cours et plus tard les données métier quand le composant en aura besoin, mais les composants, eux, sont simplement chargés une seule fois et exécutés côté client.
4 - Implementation d'un MFE en Angular
Pour créer un MFE, il suffit de créer une application javascript traditionnelle qui va déclarer un ou plusieurs “Custom Element” HTML.
En Angular, on va faire cette déclaration de la façon suivante :
- Dans le fichier app.module.js créé par défaut, je vais déclarer mon “Custom Element” que je vais nommer “sample-custom-element” de la façon suivante :
...
@NgModule({
imports: [BrowserModule],
providers: [],
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const AppComponentElement = createCustomElement(AppComponent, {
injector: this.injector,
});
customElements.define(
'sample-custom-element',
AppComponentElement
);
}
}
- Ensuite, je vais déclarer ce nouvel élément HTML dans mon fichier “index.html”, pour l'exemple je vais le mettre dans le body :
...
<body>
<sample-custom-element></sample-custom-element>
</body>
Et… c'est déjà terminé !
En compilant mon module, Angular va tout seul transformer mon index.html afin d’intégrer les fichiers nécessaires à son exécution (les fameux scripts.js, runtime.js, polyfills.js…)
Exemple avec un “Custom Element” en Angular 14 :
En Angular 19 :
Ce composant pourra être très simple, comme afficher un avatar de l'utilisateur connecté, ou bien très complexe, comme un formulaire de souscription à une assurance ou à un prêt financier.
C'est là que chaque projet doit se poser les bonnes questions en fonction de la taille de ses équipes et du niveau de maturité et complexité.
Par exemple si j'ai 3 équipes (ou 3 développeurs), chacune va travailler sur une fonctionnalité différente. Je pourrais alors décider de faire en parallèle 3 MFE et créer 3 repos GIT et 3 pipelines puis 3 images dockers contenant toutes 1 Nginx servant 4 fichiers statiques.
Petite aparté, dans les paragraphes suivants, j’en profite pour vous parler d'optimisation KISS (Keep It Simple and Stupid) du packaging des MFE.
Une question se pose: Pourquoi avoir plusieurs images si on livre à chaud sans interruption de service ? On pourrait tout à fait livrer un Nginx qui héberge tous les fichiers statiques et on livrerait directement une nouvelle image avec l'ensemble à chaque fois.
Tout comme pour GIT, utiliser un mono repo permet d'unifier le code et les modules communs. Ici l’arbitrage doit se faire entre la flexibilité de déployer chaque MFE séparément et la simplification du packaging et déploiement.
En fait, les mêmes questions de découpage se posent en micro-services, faut-il tout découper avant (pour souvent être sous-exploité) ou bien découper à l'usage lorsque un des micro-services a besoin de plus de ressources ?
Dans le cas d'un Nginx qui expose simplement des fichiers statiques, se recharge à chaud, et démarre en quelques secondes, je pense que le principe KISS s'applique complètement.
5 - Optimisation des MFE
Une fois mon premier MFE terminé, sur le même modèle, je peux soit en créer un second et l'exposer sur un autre port (ou un autre sous-domaine ou ressource) soit déclarer un second “Custom Element” dans mon projet Angular.
Ce second cas d'usage pourrait sembler bête mais dans le cas d'une librairie de composants ça peut être très utile, on va pouvoir créer tout un design system et l’importer dans chaque application distante (RemoteApp).
Ici j’emploie le terme de “RemoteApp”, c’est pour bien différencier d’un “Custom Element” qui est juste un élément de base utilisé maintes fois dans les “RemoteApps”.
Ci-dessous un exemple de deux “RemoteApp” chargeant chacun un “Custom Element” dans 2 versions d'Angular différentes.
La première “RemoteApp” serait en Angular 19 et donnerait cet écran :
On remarquera qu’elle est exposée en localhost:4300.
Alors que a seconde serait exposée en localhost:4200 :
Comme évoqué plus haut, ici elles sont exposées sur des ports différents mais il serait possible de les exposer sous un même domaine comme :
https://remote-app1.niji.fr et https://remote-app2.niji.fr
https://www.niji.fr/static/remoteApp1 et https://www.niji.fr/static/remoteApp2
De cette façon, il est possible de livrer l’un sans l’autre ou les 2 à la fois… leurs cycles de vie sont décorrélés.
Usage des caches
Cette optimisation est native et gérée par les navigateurs qui cachent ces ressources (polyfills.js, runtime.js…). A la première connexion, cette ressource sera cachée et tant qu’elle n’est pas relivrée cela ne sollicitera pas le serveur.
Et avec un simple paramètre dans l’URL, qui pourrait être un timestamp, ou un numéro de version, il sera possible de contrôler le rafraîchissement du cache des navigateurs.
Usage des “Import Maps”
Cette seconde optimisation est déjà supportée par tous les navigateurs “modernes” :
L’idée est de ne pas recharger les modules communs entre chaque “RemoteApp” qui seraient présents sur une même page ou une page différente (voire un site internet différent)
Le but est de charger tout ce qui est possible via des “imports maps” (qui pourront même être récupérés sur un CDN).
Si on l'applique à ma “RemoteApp” Angular 19, on devra ajouter ceci :
HTTP/2, gère de manière efficace la récupération de ces ressources, en les téléchargeant en parallèle et en les cachant ensuite.
Ici, tous les modules ont été externalisés dans une balise script de type “importmap” dans le “head” de la page. Autant de données qui ne seront plus dans les livrables et qui seront partagées entre toutes les “RemoteApp” Angular 19.
Pour que cela fonctionne, j'avoue que même l'IA (Copilot dans mon cas) ne m'a pas été d'une grande aide et je suis preneur si quelqu'un arrive à compiler en mode AoT et non JiT.
Le code source complet est disponible sur mon repo github : Lien vers mon repo GitHub
Pour info, il faudra désactiver le mode AoT dans le fichier “angular.json” pour que cela fonctionne… ça serait trop facile sinon 😉 !
Le principe est de changer le mode de compilation pour utiliser le mode “esbuild” et de déclarer les modules souhaités en tant que dépendances externes :
"externalDependencies": [
"@angular/animations",
"@angular/common",
"@angular/compiler",
"@angular/core",
"@angular/elements",
"@angular/forms",
"@angular/platform-browser",
"@angular/platform-browser-dynamic",
"@angular/router",
"rxjs",
"tslib",
"zone.js"
]
De cette façon Angular va comprendre qu'il ne faut pas les inclure lors de la création des livrables.
Il faudra aussi penser à modifier le fichier “tsconfig.json” pour le passer en mode “esnext”.
Voici un exemple de rendu de ces 2 “RemoteApp” unifiées dans un même écran exposé en localhost:8080 :
Vous remarquerez que l’écran contient les 2 “RemoteApp”, l’une en Angular 14 et l’autre en Angular 19, de plus, elles sont “brandées” aux couleurs du site en cours, qui est celui de “Canon” puisque c’est la feuille de styles de “Canon” qui est appliquée (rouge avec une pastille orange).
Alors que lorsqu’elles sont exposées seules, elles sont de couleurs bleues avec une pastille rouge.
6 - Conclusion
En conclusion, il va falloir attendre quelques semaines avant que je vous explique comment simplifier tout ça et le rendre modifiable et configurable à chaud.
Pour ceux qui ne m'auraient pas vu venir c'est là qu'entre en scène Liferay, qui aussi bien avec sa version DXP ou CE, pourra grandement nous faciliter la gestion et l'interaction entre toutes ces RemoteApps.
Plus besoin de gérer les styles, les permissions, l'authentification, la configuration, la GED, le SSO, la recherche, la génération de code… tout ça, Liferay le fera nativement et chaque développeur pourra se concentrer sur sa partie métier (front ou back)
Sachant que pour le back, Liferay propose déjà du LowCode/NoCode, exposé en REST et GraphQL
✌️ Merci de m’avoir lu, et à très bientôt pour la deuxième partie !
Subscribe to my newsletter
Read articles from Eric DARIEL directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
