Les débats d’architecture web tournent trop souvent autour des technologies — React ou Vue, REST ou GraphQL, monolithe ou microservices. La question structurante n’est pas là. Elle est plus simple et plus difficile à la fois : quelle couche porte quelle responsabilité, et quel risque est introduit quand cette responsabilité est au mauvais endroit ? Une application web moderne opère sur trois couches aux caractéristiques physiques radicalement différentes — Edge, Serveur, Client. Chaque couche a un coût propre, une surface de cache propre, et des contraintes propres. Les problèmes de performance et de maintenabilité les plus courants sont des problèmes de placement de responsabilité, pas des problèmes de technologie.
Les trois couches d’une architecture web moderne
Avant de raisonner sur les responsabilités, il faut nommer les couches et comprendre leurs contraintes physiques. Ce ne sont pas des choix techniques — ce sont des réalités de l’infrastructure réseau.
Géographiquement distribué, au plus proche des utilisateurs. Statique-first : sert du HTML, JS, CSS, images pré-générés. Latence quasi nulle pour les assets en cache. Coût minimal par requête. Limite : ne peut pas exécuter de logique complexe ou accéder à des données dynamiques sans compromis. Cas d’usage idéal : assets statiques, pages pré-rendues (ISR/SSG), redirections, headers de sécurité.
Réseau interne entre services — la latence est négligeable (1ms vs 80ms côté client). Orchestre, agrège, transforme les données. Peut paralléliser des appels vers plusieurs sources sans coût réseau visible. Différencie les stratégies de cache par type de donnée. Cas d’usage idéal : orchestration, composition de données, logique métier, authentification, agrégation de sources multiples.
Chaque appel réseau est un round-trip complet — latence + temps de traitement serveur + latence retour. Les appels séquentiels s’accumulent — deux appels à 80ms de latence = 160ms minimum incompressibles. Capacités variables selon l’appareil. Cas d’usage idéal : interactivité, état local de l’UI, appels déclenchés par une action utilisateur, personnalisation post-chargement.
La règle physique qu’on oublie : un appel réseau côté client coûte un round-trip complet — 80ms sur 4G dans les bonnes conditions, 200ms+ sur réseau dégradé ou mobile. Le même appel côté serveur, vers un service interne, coûte 1ms en réseau interne. Ce n’est pas un détail d’optimisation — c’est une contrainte physique qui doit guider les décisions d’architecture avant que le code soit écrit.
Les quatre responsabilités à répartir entre les couches
Une architecture web moderne doit allouer explicitement quatre types de responsabilités. Les problèmes apparaissent quand ces allocations sont faites implicitement — par défaut, par commodité, ou par héritage d’une architecture précédente.
| Responsabilité | Description | Couche naturelle | Risque si mal placée |
|---|---|---|---|
| Orchestration | Coordonner plusieurs sources de données pour construire une réponse | Serveur — réseau interne, parallélisation sans coût visible | Si côté client : waterfall réseau, multiplication des round-trips, latence additionnée |
| Composition | Assembler les données en une structure consommable par la couche de présentation | Serveur (BFF) ou Edge (ISR) — une seule réponse cohérente au client | Si côté client : logique dupliquée, couplage implicite entre équipes, logique interne exposée dans les réponses publiques |
| Cache | Stocker et invalider les données selon leur fréquence de changement | Edge pour les données froides, Serveur pour les données tièdes, Client pour l’état UI | Un TTL unique pour toutes les données ignore leur nature — données statiques revalidées inutilement, données dynamiques trop cachées |
| Hydratation | Activer l’interactivité sur un HTML déjà rendu | Client — par définition, mais à minimiser (Islands Architecture, Server Components) | Over-hydration : tout le JS envoyé au client, long main-thread blocking, TBT dégradé |
Ces allocations ne sont pas définitives — elles évoluent avec le produit. Une page dont les données changent toutes les heures peut être servie depuis l’Edge avec de l’ISR. Si ces mêmes données commencent à changer en temps réel, la responsabilité de cache se déplace vers le serveur. L’architecture suit la nature des données, pas l’inverse.
Le glissement de responsabilité le plus fréquent — l’orchestration côté client
Le pattern le plus répandu — et le plus coûteux en performance — est l’orchestration côté client : le navigateur fait plusieurs appels réseau séquentiels là où le serveur aurait pu les faire en parallèle, en interne, en une fraction du temps.
## Exemple : page catégorie e-commerce avec CMS headless
Pattern actuel — orchestration côté client :
t=0ms → navigateur charge la page
t=80ms → Call 1 : GET /api/pages/cat-chaussures (config, composants, SEO)
t=200ms → réponse Call 1 reçue — le client sait maintenant quoi demander
t=280ms → Call 2 : GET /api/products?cat=chaussures (les produits)
t=460ms → réponse Call 2 reçue — premier produit visible
→ Total : ~460ms incompressibles sur 4G
Pattern corrigé — orchestration côté serveur (BFF) :
t=0ms → navigateur charge la page
t=80ms → Call unique : GET /api/category-page/chaussures
↳ BFF fait en parallèle (~1ms réseau interne) :
· call CMS pour la config
· call products pour les produits
t=280ms → réponse unique reçue — page complète
→ Total : ~280ms — soit ~40% de gain sur le Time to Interactive
Risque structurel
Le waterfall de performance — incompressible par nature
Le waterfall réseau côté client n’est pas un bug — c’est une conséquence directe du placement de responsabilité. Le Call 2 ne peut pas démarrer avant que le Call 1 soit résolu, parce que le client ne sait pas ce qu’il doit demander avant d’avoir reçu la réponse du Call 1. Ce n’est pas corrigeable par de l’optimisation fine (compression, CDN, lazy loading) — seul un déplacement de la responsabilité d’orchestration vers le serveur le résout. Les optimisations de surface masquent le problème sans l’éliminer.
Risque secondaire
Le couplage implicite et l’exposition de logique interne
Quand l’orchestration est côté client, la réponse du premier appel doit contenir tout ce dont le client a besoin pour décider quoi demander ensuite — feature flags, configuration de composants, paramètres d’A/B tests, règles de routing. Cette logique interne, exposée dans une réponse publique, agrandit la surface d’attaque et crée un couplage implicite entre les équipes front et back. Chaque évolution du schéma nécessite une coordination — et parfois des déploiements synchronisés.
Les patterns qui repositionnent les responsabilités dans les bonnes couches
Trois patterns résolvent le problème d’orchestration côté client — avec des compromis différents selon le contexte.
BFF (Backend For Frontend) — l’orchestration centralisée
Un BFF est un serveur dédié à un client (web, mobile, TV) qui agrège les données de plusieurs services et retourne une réponse composée et adaptée. Le client fait un seul appel ; l’orchestration se passe côté serveur où le réseau interne est négligeable.
// BFF — orchestration en parallèle, réseau interne
app.get('/api/category-page/:slug', async (req, res) => {
const [config, products] = await Promise.all([
cmsService.getPageConfig(req.params.slug), // ~1ms réseau interne
productService.getProducts(req.params.slug), // ~1ms réseau interne
]);
res.json(composePageResponse(config, products));
// Le client ne connaît pas les sources internes
});
Le BFF possède le contrat de composition et peut différencier les TTL de cache par type de donnée — long pour la config éditoriale, court pour les stocks et les prix. Compromis : complexité infra additionnelle, point de défaillance à gérer, besoin d’équipe back pour le maintenir.
Streaming SSR avec Suspense — le progressif sans round-trip
React 18 + Next.js App Router permettent de streamer le HTML par morceaux depuis le serveur. La structure arrive immédiatement, les données lentes arrivent streamées — sans round-trip client supplémentaire. Le skeleton affiché est un vrai résultat de streaming, pas l’attente d’un second appel.
// Server Component — le fetch se passe côté serveur, pas côté client
export default function Page({ params }) {
return (
<PageShell slug={params.slug}> // rendu immédiat, pas de fetch
<Suspense fallback={<ProductSkeleton />}>
<ProductGrid slug={params.slug} /> // streamé depuis le serveur
</Suspense>
</PageShell>
);
}
async function ProductGrid({ slug }) {
const products = await fetchProducts(slug); // 1ms réseau interne
return <Grid items={products} />;
}
La responsabilité reste côté serveur. Le client reçoit du HTML progressivement sans jamais faire d’appel supplémentaire. Compromis : couplage au framework (React 18+ / Next.js App Router), complexité du modèle mental pour les équipes qui débutent avec les Server Components.
ISR et cache différencié — éliminer les appels sur les données froides
Quand les données changent rarement (structure de page, contenus éditoriaux), elles peuvent être pré-générées et servies depuis le CDN. ISR (Incremental Static Regeneration) revalide en arrière-plan selon un TTL sans bloquer les utilisateurs.
// ISR — config en cache 1h, produits dynamiques en parallèle
export const revalidate = 3600; // revalidation toutes les heures
export default async function Page({ params }) {
const [config, products] = await Promise.all([
fetchPageConfig(params.slug), // cache CDN ~5ms
fetchProducts(params.slug), // call dynamique ~200ms, en parallèle
]);
}
La config de page devient un cache hit à 5ms. Seul le call produits est dynamique — et il part en parallèle, pas après. Compromis : gestion de l’invalidation (trop lente = données obsolètes, trop fréquente = surcharge origine), coordination du déploiement quand la config et le code changent ensemble.
Matrice de décision — quand choisir quelle couche
| Pattern | Couche | Responsabilité | Risque principal | Quand l’utiliser |
|---|---|---|---|---|
| Double-call client séquentiel | Client | Dispersée | Waterfall + couplage implicite | Héritage à refactorer |
Promise.all côté client |
Client | Mieux — parallèle | Toujours 2 round-trips, logique interne exposée | Acceptable si les deux sources sont indépendantes et connues à l’avance |
| BFF | Serveur | Centralisée | Complexité infra, point de défaillance | Agrégation non triviale de sources multiples |
| Streaming SSR + Suspense | Serveur | Centralisée | Couplage au framework | Données à latences différentes, UX progressive prioritaire |
| ISR + fetch parallèle | Edge + Serveur | Hybride | Invalidation de cache, stale data | Données éditoriales stables séparables des données dynamiques |
Nuances — ce que cette analyse ne dit pas
Déplacer les responsabilités dans les bonnes couches résout des problèmes de performance et de maintenabilité — et en crée d’autres si le déplacement n’est pas adapté au contexte.
La complexité infra a un coût réel
Un BFF est un service de plus à déployer, monitorer, scaler, et sécuriser. Une équipe de trois développeurs full-stack sans DevOps dédié peut ne pas avoir la capacité de maintenir un BFF correctement. Dans ce contexte, un Promise.all côté client avec deux appels parallèles peut être un compromis raisonnable — pas idéal, mais proportionné à la maturité opérationnelle de l’équipe. L’architecture parfaite qui n’est pas maintenue est moins bonne que l’architecture imparfaite qui tourne.
Le double-call client n’est pas toujours un anti-pattern
Il existe des cas où l’orchestration côté client est délibérée et justifiée : une page de tableau de bord qui charge des widgets indépendants en parallèle, chacun gérant son propre état de chargement et d’erreur. Ou un système de personnalisation post-chargement qui doit s’exécuter côté client pour accéder aux données utilisateur sans les exposer dans le HTML servi. Le problème n’est pas l’appel client en soi — c’est l’appel client séquentiel pour des données qui auraient pu être agrégées côté serveur. Parallèle et délibéré vs séquentiel et accidentel : la distinction est importante.
L’architecture suit la nature des données — pas les modes
ISR fonctionne bien pour des données qui changent rarement — et mal pour des données qui changent en temps réel. Streaming SSR est optimal pour des pages avec des zones de latence différente — et sur-complexe pour des pages simples avec une seule source de données. BFF est justifié quand plusieurs services doivent être agrégés — et de la sur-ingénierie pour une API unique. La bonne décision d’architecture est celle qui correspond à la fréquence de changement des données, à la tolérance à la latence de l’utilisateur, et à la capacité de maintenance de l’équipe — dans cet ordre.
Questions fréquentes sur l’architecture web moderne
Qu’est-ce qu’un BFF (Backend For Frontend) et quand faut-il en créer un ?
Un BFF est un service serveur dédié à un client spécifique (web, mobile, TV) qui agrège les données de plusieurs services backend et retourne une réponse composée et adaptée. Il déplace l’orchestration du client (où chaque appel coûte un round-trip réseau complet) vers le serveur (où les appels inter-services coûtent 1ms en réseau interne). Il faut en créer un quand plusieurs sources de données doivent être agrégées, quand la logique de composition est non triviale, ou quand plusieurs clients ont besoin de représentations différentes des mêmes données. Ne pas en créer un si l’équipe n’a pas la capacité de le maintenir comme un service de production — un Promise.all côté client reste préférable à un BFF mal maintenu.
Quelle est la différence entre SSR, SSG, et ISR en termes d’architecture ?
Ces trois stratégies correspondent à des allocations différentes de la responsabilité de rendu. SSR (Server-Side Rendering) génère le HTML à chaque requête côté serveur — adapté aux données dynamiques par utilisateur (tableaux de bord personnalisés, pages de compte). SSG (Static Site Generation) génère le HTML une fois au build — adapté aux données très stables (documentation, pages marketing). ISR (Incremental Static Regeneration) génère le HTML statiquement et le revalide en arrière-plan selon un TTL — adapté aux données tièdes qui changent toutes les heures (pages produit, articles de blog). Le choix dépend de la fréquence de changement des données et de la tolérance à voir des données légèrement obsolètes — pas du framework utilisé.
Comment identifier un waterfall réseau côté client et comment le corriger ?
Un waterfall réseau côté client se diagnostique dans les DevTools réseau du navigateur : si deux requêtes sont séquentielles (la deuxième commence après la fin de la première) sans action utilisateur entre les deux, c’est un waterfall. Pour corriger : si les deux requêtes peuvent être déclenchées en parallèle, utiliser Promise.all côté client. Si l’une ne peut démarrer qu’une fois l’autre résolue parce que sa réponse contient des paramètres nécessaires, déplacer l’orchestration côté serveur (BFF ou Server Component) où les deux appels sont parallélisés en réseau interne. Ne jamais accepter un waterfall comme inévitable — c’est toujours une décision architecturale à remettre en question.
Qu’est-ce que le streaming SSR avec Suspense et en quoi améliore-t-il les performances ?
Le streaming SSR avec Suspense (React 18 + Next.js App Router) permet d’envoyer le HTML depuis le serveur par morceaux, progressivement — sans attendre que toutes les données soient disponibles. Le navigateur affiche d’abord la structure de page (shell, navigation, squelettes), puis les zones dépendant de données lentes arrivent streamées au fil de leur disponibilité côté serveur. L’amélioration de performance vient de deux sources : le TTFB est plus court car le HTML commence à arriver immédiatement, et il n’y a pas de round-trip client supplémentaire — les données arrivent dans le même flux HTTP que la page. La différence avec le skeleton loading classique : le skeleton classique attend un second appel client (waterfall), le skeleton Suspense est un vrai résultat de streaming depuis le serveur.
Comment structurer le cache dans une architecture web multi-couches ?
Le cache doit être différencié par type de donnée et par couche, selon la fréquence de changement et le coût d’une donnée obsolète. Les assets statiques (images, polices, JS) vont en cache CDN avec un TTL long et un cache-busting par hash. Les données éditoriales stables (structure de page, contenus marketing) vont en cache CDN avec ISR et un TTL de quelques heures. Les données produit (prix, stocks) vont en cache serveur avec un TTL court (minutes) ou sans cache selon la criticité. Les données utilisateur (panier, préférences) ne vont pas en cache CDN — elles doivent passer par un serveur authentifié. L’erreur fréquente est d’utiliser un TTL unique pour toutes les données, ou de ne pas invalider le cache quand les données changent hors du cycle de déploiement.
Comment choisir entre architecture monolithique et microservices pour le backend ?
La question n’est pas « monolithe ou microservices » — c’est « à quel stade de maturité sommes-nous, et quelle complexité opérationnelle pouvons-nous absorber ? ». Un monolithe bien structuré (modules clairs, dépendances explicites, tests solides) est plus rapide à développer, plus simple à déployer et déboguer, et suffisant pour la majorité des produits jusqu’à des dizaines de millions d’utilisateurs. Les microservices résolvent des problèmes de scalabilité indépendante, de déploiement indépendant, et d’isolation de domaine à l’échelle — ils introduisent de la complexité réseau, opérationnelle, et des problèmes de cohérence distribuée. Règle pratique : commencer par un monolithe modulaire, extraire en services uniquement quand la douleur d’un module monolithique est prouvée — pas anticipée.
Conclusion
L’architecture web moderne n’est pas une question de technologies — c’est une question de placement de responsabilités. L’orchestration appartient au serveur, où le réseau interne est négligeable. Le cache doit être différencié par la nature des données, pas appliqué uniformément. L’hydratation doit être minimisée, pas assumée. Chaque responsabilité mal placée introduit un coût — en performance, en couplage, ou en maintenabilité — qui s’accumule silencieusement jusqu’à ce qu’il devienne indéniable.
Comprendre une architecture, c’est savoir nommer ce que chaque couche fait — et identifier le risque introduit quand une responsabilité est placée au mauvais endroit. Ce diagnostic précède toujours la solution technique.
