Depuis une quinzaine d’années, une part croissante du web est construite sous forme de Single Page Applications, ou SPA : des applications qui chargent une seule page HTML puis gèrent toute la navigation en JavaScript, en changeant le contenu et l’adresse sans jamais recharger la page. React, Vue, Angular, Svelte et les frameworks construits par-dessus, comme Next.js et Nuxt, sont devenus les outils standard de nombreux projets, y compris de sites qui auraient autrefois été construits avec un CMS traditionnel. Même dans le monde WordPress, l’approche « headless », avec WordPress comme source de contenu et un front-end JavaScript, est de plus en plus courante.
Les Core Web Vitals, cependant, ont été conçus en pensant au modèle traditionnel, où chaque clic sur un lien charge une nouvelle page. Appliqués à une SPA, ils produisent des effets inattendus : des métriques qui ne mesurent que la première page, des valeurs qui s’accumulent sur toute la session, de grandes différences entre ce que l’utilisateur perçoit et ce que rapportent les données. Dans cet article, nous voyons comment se comportent les métriques dans les SPA, quels sont les problèmes de performance typiques de ce modèle, comment les frameworks modernes y répondent et comment mesurer réellement l’expérience.
Navigations « hard » et « soft »
Sur un site traditionnel, chaque clic sur un lien produit une navigation complète : le navigateur demande une nouvelle page au serveur, abandonne la précédente et construit la nouvelle à partir de zéro. Chaque navigation a son propre LCP, son propre CLS et son propre INP, mesurés et attribués à l’adresse de cette page. C’est le modèle pour lequel les Core Web Vitals ont été conçus, et celui sur lequel repose le Chrome UX Report.
Dans une SPA, seule la première visite est une navigation complète, dite « hard ». Les clics suivants sont des navigations « soft » : le JavaScript intercepte le clic, télécharge les données dont il a besoin, met à jour une partie de la page et change l’adresse avec l’History API, par history.pushState(). Pour l’utilisateur, l’expérience est celle d’une nouvelle page ; pour le navigateur, du point de vue des métriques, c’est toujours la même page qui change de contenu.
Les conséquences pour les métriques sont au nombre de trois. Le LCP n’est mesuré que sur la première page : les navigations soft, même lentes, ne produisent pas de nouveau LCP dans les données de terrain. Le CLS et l’INP, en revanche, sont observés pendant toute la vie de la page, c’est-à-dire pendant toute la session de l’utilisateur dans la SPA, et attribués à l’adresse avec laquelle la session a commencé. Un utilisateur qui entre par la page d’accueil, navigue dix minutes entre catégories et produits et tombe sur une interaction lente dans le panier apporte cet INP lent aux données de la page d’accueil.
Le résultat est un tableau déformé dans deux directions. D’un côté, le LCP des SPA tend à paraître meilleur que ne l’est réellement l’expérience globale, parce que les navigations suivantes, que l’utilisateur perçoit comme des chargements, ne sont pas mesurées. De l’autre, le CLS et l’INP des pages d’atterrissage tendent à paraître pires, parce qu’ils recueillent des problèmes qui se produisent ailleurs et sur des sessions très longues.
Mesurer les navigations soft
Le problème est bien connu, et l’équipe de Chrome travaille depuis longtemps sur une solution : un mécanisme qui reconnaît les navigations soft, quand une interaction de l’utilisateur change l’adresse et met à jour le contenu de façon significative, et les traite comme des navigations à part entière, avec leur propre LCP, CLS et INP. Le mécanisme a été expérimenté en plusieurs phases et la bibliothèque web-vitals en propose une prise en charge expérimentale. Au moment de la rédaction, il ne fait cependant pas partie des Core Web Vitals officiels, il n’est pas utilisé dans le Chrome UX Report et il n’influence pas l’évaluation de Google.
Tant qu’il ne sera pas devenu un standard, celui qui exploite une SPA a deux options complémentaires : mesurer lui-même les navigations soft, avec un système de Real User Monitoring qui enregistre le temps entre le clic et la fin de la mise à jour, et interpréter les données officielles en sachant ce qu’elles incluent et ce qu’elles laissent de côté.
// Mesure simplifiée d’une navigation soft
router.beforeEach( () => {
performance.mark( 'navigazione-inizio' );
} );
router.afterEach( async () => {
await new Promise( ( r ) => requestAnimationFrame( () => setTimeout( r, 0 ) ) );
performance.mark( 'navigazione-fine' );
const m = performance.measure( 'navigazione-soft', 'navigazione-inizio', 'navigazione-fine' );
invia( { nome: 'soft-nav', valore: m.duration, pagina: location.pathname } );
} );
Le premier chargement : le rendu côté client
Le problème de performance le plus classique des SPA concerne la première page. Dans sa forme la plus simple, le rendu côté client, le serveur envoie un document HTML presque vide, avec un conteneur et une référence à un gros fichier JavaScript. Le navigateur doit télécharger ce fichier, l’analyser et l’exécuter ; le code doit demander des données à une API, attendre la réponse et seulement alors construire la page. Jusqu’à ce moment, l’utilisateur voit une page blanche ou un squelette de chargement.
Sur un ordinateur rapide avec une bonne connexion, la chaîne peut sembler rapide ; sur un smartphone de milieu de gamme, avec des centaines de kilooctets de JavaScript à exécuter, le LCP peut facilement dépasser trois ou quatre secondes. Le navigateur, de plus, ne peut pas découvrir l’image principale à l’avance, car elle n’existe pas dans le HTML : elle est insérée par le JavaScript au bout de la chaîne, et aucune des optimisations vues dans l’article sur le LCP, du preload scanner à la haute priorité, ne peut agir.
Rendu côté serveur et hydratation
La réponse des frameworks modernes est le rendu côté serveur : le serveur exécute le même code d’application, produit un document HTML complet et l’envoie au navigateur, qui peut l’afficher tout de suite. Le contenu et l’image principale sont dans le document, et le LCP redevient comparable à celui d’un site traditionnel. Quand les pages peuvent être générées à l’avance, au moment de la publication, on parle de génération statique, avec des résultats encore meilleurs.
Mais le HTML généré sur le serveur n’est qu’une image de la page : pour le rendre interactif, le navigateur doit encore télécharger et exécuter le JavaScript de l’application, qui reconstruit en mémoire l’état des composants et attache les gestionnaires d’événements aux éléments existants. Ce processus s’appelle l’hydratation, et sur des pages complexes il peut représenter des centaines de millisecondes de travail sur le thread principal.
Entre le moment où la page est visible et celui où elle devient interactive, une sorte de vallée s’ouvre : l’utilisateur voit des boutons et des menus, les touche, mais rien ne se passe, ou la réponse arrive très tard parce que le thread principal est occupé par l’hydratation. Ce scénario dégrade directement l’INP, et il est particulièrement frustrant parce que la page a l’air prête.
Les réponses des frameworks modernes
Ces dernières années, les frameworks ont développé plusieurs stratégies pour réduire le coût de l’hydratation, avec un principe commun : envoyer moins de JavaScript au navigateur.
- L’architecture en îlots. Des frameworks comme Astro génèrent des pages entièrement statiques et ne rendent interactives que les parties qui en ont besoin, les « îlots », comme un panier ou un formulaire de recherche. Le reste de la page n’est pas hydraté et n’exige aucun JavaScript.
- Les composants serveur. Les React Server Components, utilisés par exemple par Next.js, permettent à certains composants de ne s’exécuter que sur le serveur : leur code n’est jamais envoyé au navigateur, et seuls les composants interactifs sont hydratés.
- L’hydratation progressive et sélective. Plusieurs frameworks hydratent d’abord les composants visibles ou avec lesquels l’utilisateur interagit, et reportent le reste.
- La resumability à la place de l’hydratation. Des approches comme celle de Qwik évitent complètement l’hydratation : l’état est sérialisé dans le HTML et le code d’un composant n’est téléchargé que lorsqu’il le faut, au premier clic.
- Le streaming. Le serveur envoie le HTML par morceaux, à mesure que les données sont prêtes, de sorte que le navigateur peut afficher tout de suite les parties rapides de la page sans attendre les plus lentes.
Aucune de ces stratégies n’est automatique : une application Next.js peut être légère comme une plume ou très lourde selon la façon dont elle est écrite. Mais le bon framework, utilisé en connaissance de cause, permet d’atteindre des performances comparables à celles d’un bon site traditionnel même avec une architecture moderne.
Les problèmes des navigations soft
L’avantage que promettent les SPA est une navigation fluide : pas de rechargements, des transitions rapides, un état conservé. C’est un avantage réel quand l’application est bien construite, mais plusieurs pratiques courantes l’annulent.
- Les cascades de requêtes. Le composant de la nouvelle page est chargé, puis il demande ses données, puis les composants enfants demandent d’autres données. Chaque étape attend la précédente, et la navigation soft peut durer plus longtemps qu’un chargement complet. La solution est de charger code et données en parallèle, dès le survol ou le clic.
- Les décalages de mise en page. La nouvelle page apparaît par morceaux, d’abord la structure, puis les images sans dimensions, puis les données : chaque arrivée déplace le contenu, et ces décalages s’additionnent dans le CLS de la session.
- Aucun retour au clic. Si le clic lance le chargement mais que la page ne change pas avant que tout soit prêt, l’utilisateur ne peut pas savoir si le clic a fonctionné. Un retour immédiat, même un simple indicateur de chargement, améliore la perception et l’INP.
- Mémoire et sessions longues. Des composants qui ne sont pas retirés correctement, des écouteurs d’événements qui s’accumulent, des données conservées en mémoire et jamais libérées : dans les sessions longues, l’application ralentit peu à peu, et chaque interaction devient plus lourde.
- Position de défilement et focus. Après une navigation soft, la page doit revenir en haut, et le focus doit se déplacer de façon cohérente pour les personnes qui utilisent un clavier ou un lecteur d’écran. Quand ce n’est pas le cas, l’expérience se dégrade même si les métriques semblent bonnes.
Le cas de WordPress headless
Une architecture de plus en plus courante utilise WordPress uniquement comme système de gestion de contenu, avec un contenu exposé par l’API REST ou GraphQL, et un front-end séparé construit avec un framework JavaScript. Les avantages annoncés sont la flexibilité, la sécurité et la performance. La performance, cependant, dépend entièrement de la manière dont le front-end est construit : un site headless avec génération statique et peu de JavaScript peut être extrêmement rapide ; un autre avec rendu côté client et un gros bundle applicatif peut être bien plus lent que le site WordPress traditionnel qu’il a remplacé.
Avant de choisir cette voie pour des raisons de performance, il vaut la peine de la comparer à ce qu’un site WordPress traditionnel bien optimisé peut déjà offrir : cache de page, thème léger, peu de scripts, et les techniques modernes qui rendent la navigation entre les pages presque instantanée même sans SPA.
L’alternative : des pages traditionnelles avec des outils modernes
L’une des raisons d’origine du choix d’une SPA était la fluidité des transitions entre les pages. Aujourd’hui, le navigateur offre des outils qui rendent cette fluidité possible même avec des pages traditionnelles, sans les coûts d’une application JavaScript. Les Speculation Rules permettent de charger la page suivante à l’avance et de l’ouvrir instantanément ; le back/forward cache rend immédiat le retour aux pages visitées ; les View Transitions entre documents, prises en charge dans les navigateurs récents, permettent des animations fluides d’une page à l’autre avec quelques lignes de CSS.
/* Transitions animées entre pages traditionnelles, sans JavaScript */
@view-transition {
navigation: auto;
}
.copertina-articolo {
view-transition-name: copertina;
}
Pour de nombreux sites de contenu, catalogues et sites d’entreprise, la combinaison de pages traditionnelles générées sur le serveur, d’un cache efficace, du prerendering et de transitions animées offre une expérience comparable à celle d’une SPA, avec des métriques mesurées correctement pour chaque page et beaucoup moins de JavaScript. Les SPA restent le choix naturel pour les vraies applications, comme les tableaux de bord, les éditeurs et les outils avec beaucoup d’état partagé entre les écrans.
Comment mesurer une SPA
Pour évaluer correctement une SPA, il faut plusieurs niveaux de mesure. Les données du Chrome UX Report restent la référence pour le premier chargement et pour la façon dont Google voit le site, mais il faut les lire en sachant que le CLS et l’INP incluent toute la session. Un système de RUM devrait enregistrer séparément les métriques de la navigation initiale et la durée des navigations soft, avec l’adresse de destination, et pour l’INP l’élément et la page où l’interaction a eu lieu, pas seulement l’adresse d’entrée.
Dans les tests de laboratoire, outre le chargement initial, il vaut la peine d’enregistrer quelques navigations typiques avec le panneau Performance des DevTools, avec la limitation du processeur activée : cliquer sur un produit depuis la catégorie, ajouter au panier, passer à la commande. Ce sont les moments où l’utilisateur juge la fluidité de l’application, et les tests traditionnels, centrés sur le premier chargement, ne les voient pas.
En résumé
Les Single Page Applications changent la manière dont les Core Web Vitals décrivent l’expérience : le LCP ne mesure que le premier chargement, tandis que le CLS et l’INP s’accumulent sur toute la session et sont attribués à la page d’entrée. Chrome travaille sur une mesure dédiée aux navigations soft, mais pour l’instant, celui qui exploite une SPA doit compléter les données officielles par ses propres mesures.
Côté performance, le rendu côté client pénalise le premier chargement, le rendu côté serveur le résout mais introduit le coût de l’hydratation, et les frameworks modernes le réduisent en envoyant moins de JavaScript avec les îlots, les composants serveur et l’hydratation sélective. Pour de nombreux sites de contenu, toutefois, la voie la plus simple vers une expérience fluide passe aujourd’hui par des pages traditionnelles enrichies du prerendering, du back/forward cache et des View Transitions : la fluidité d’une SPA, avec la légèreté et la mesurabilité d’un site classique.