Rendu sur le Web

Où devrions-nous implémenter la logique et le rendu dans nos applications ? Devons-nous utiliser le rendu côté serveur ? Et la réhydratation ? Trouvons des réponses !

En tant que développeurs, nous sommes souvent confrontés à des décisions qui affecteront l'ensemble de l'architecture de nos applications. L'une des principales décisions que doivent prendre les développeurs Web concerne l'implémentation de la logique et de l'affichage dans leur application. Cela peut être difficile, car il existe de nombreuses façons de créer un site Web.

Notre compréhension de cet espace s'appuie sur le travail que nous avons accompli dans Chrome pour communiquer avec des sites de grande envergure ces dernières années. De manière générale, nous encourageons les développeurs à privilégier le rendu côté serveur ou le rendu statique plutôt qu'une approche de réhydratation complète.

Afin de mieux comprendre les architectures que nous allons choisir lorsque nous prendrons cette décision, nous devons avoir une bonne compréhension de chaque approche et une terminologie cohérente à utiliser. Les différences entre ces approches permettent d'illustrer les compromis liés à l'affichage sur le Web du point de vue des performances.

Terminologie

Rendu

  • Rendu côté serveur : affichage au format HTML d'une application côté client ou universelle sur le serveur
  • Rendu côté client (CSR) : affichage d'une application dans un navigateur via JavaScript pour modifier le DOM
  • Réhydratation : "démarre" les vues JavaScript sur le client de sorte qu'elles réutilisent l'arborescence DOM et les données du code HTML rendu par le serveur.
  • Prérendu:exécution d'une application côté client au moment de la compilation pour capturer son état initial sous forme de code HTML statique.

Performances

Rendu côté serveur

Le rendu côté serveur génère l'intégralité du code HTML d'une page sur le serveur en réponse à la navigation. Cela évite des allers-retours supplémentaires pour l'extraction des données et la création de modèles sur le client, car ces opérations sont gérées avant que le navigateur n'obtienne une réponse.

Le rendu côté serveur produit généralement un FCP rapide. L'exécution de la logique de la page et de l'affichage sur le serveur permet d'éviter d'envoyer beaucoup de code JavaScript au client. Cela permet de réduire la requête de navigation détaillée d'une page, ce qui peut également entraîner une baisse de l'INP, car le thread principal n'est pas bloqué aussi souvent qu'au moment du chargement de la page. Lorsque le thread principal est bloqué moins souvent, les interactions utilisateur ont plus de chances de s'exécuter plus tôt. C'est logique, car avec le rendu côté serveur, vous n'envoyez en fait que du texte et des liens au navigateur de l'utilisateur. Cette approche peut s'avérer efficace pour un large éventail de conditions liées aux appareils et aux réseaux, et offre des optimisations intéressantes pour les navigateurs, telles que l'analyse de documents en flux continu.

Schéma illustrant le rendu côté serveur et l'exécution JS affectant le FCP et le TTI.

Avec le rendu côté serveur, les utilisateurs sont moins susceptibles d'attendre l'exécution du code JavaScript lié au processeur avant de pouvoir accéder à votre site. Même s'il est impossible d'éviter l'utilisation de JavaScript tiers, l'utilisation du rendu côté serveur pour réduire vos propres coûts JavaScript propriétaires peut vous permettre de bénéficier d'un budget plus important pour le reste. Toutefois, cette approche présente un compromis potentiel: la génération de pages sur le serveur prend du temps, ce qui peut entraîner un TTFB plus élevé.

Le fait que le rendu côté serveur soit suffisant pour votre application dépend en grande partie du type d'expérience que vous développez. Les bonnes applications du rendu côté serveur et du rendu côté client font l'objet de débats de longue date. Toutefois, il est important de se rappeler que vous pouvez choisir d'utiliser le rendu côté serveur pour certaines pages et pas pour d'autres. Certains sites ont réussi à adopter des techniques de rendu hybride. Le serveur Netflix affiche ses pages de destination relativement statiques, tout en préchargeant le code JavaScript pour les pages comportant beaucoup d'interactions, ce qui augmente les chances de chargement rapide de ces pages plus lourdes affichées par le client.

De nombreux frameworks, bibliothèques et architectures modernes permettent d'afficher la même application à la fois sur le client et sur le serveur. Vous pouvez utiliser ces techniques pour le rendu côté serveur. Toutefois, il est important de noter que les architectures dans lesquelles le rendu s'effectue à la fois sur le serveur et sur le client constituent leur propre classe de solution, avec des caractéristiques de performances et des compromis très différents. Les utilisateurs de React peuvent utiliser les API DOM serveur ou des solutions intégrées comme Next.js pour le rendu côté serveur. Les utilisateurs de Vue peuvent consulter le guide de rendu côté serveur ou Nuxt. Angular comporte la mention Universal. Les solutions les plus populaires utilisent cependant une certaine forme d'hydratation, alors soyez conscient de l'approche utilisée avant de choisir un outil.

Affichage statique

Le rendu statique s'effectue au moment de la compilation. Cette approche offre un FCP rapide, ainsi qu'une valeur de transaction non personnalisée et d'INP plus faible, en supposant que la quantité de fichiers JS côté client est limitée. Contrairement au rendu côté serveur, il permet également d'atteindre un TTFB constamment rapide, car le code HTML d'une page n'a pas besoin d'être généré dynamiquement sur le serveur. En général, l'affichage statique implique de produire à l'avance un fichier HTML distinct pour chaque URL. Grâce aux réponses HTML générées à l'avance, les rendus statiques peuvent être déployés sur plusieurs CDN afin de bénéficier de la mise en cache périphérique.

Schéma illustrant l'affichage statique et l'exécution JS facultative affectant le FCP et le TTI.

Les solutions pour le rendu statique sont de toutes formes et de toutes tailles. Grâce à des outils tels que Gatsby, les développeurs ont l'impression que leur application est affichée de manière dynamique au lieu d'être générée au cours d'une étape de compilation. Les outils de génération de sites statiques tels que 11ty, Jekyll et Metalsmith s'appuient sur leur nature statique et adoptent une approche davantage basée sur des modèles.

L'un des inconvénients de l'affichage statique est que des fichiers HTML individuels doivent être générés pour chaque URL possible. Cela peut s'avérer difficile, voire impossible, lorsque vous ne pouvez pas prédire ce que seront ces URL à l'avance, ou pour des sites comportant un grand nombre de pages uniques.

Les utilisateurs de React connaissent peut-être Gatsby, l'exportation statique Next.js ou Navi, qui facilitent la création de pages à l'aide de composants. Cependant, il est important de comprendre la différence entre l'affichage statique et le prérendu: les pages rendues statiques sont interactives sans qu'il soit nécessaire d'exécuter beaucoup de code JavaScript côté client, tandis que le prérendu améliore le FCP d'une application monopage qui doit être démarrée sur le client pour que les pages soient vraiment interactives.

Si vous ne savez pas si une solution donnée est l'affichage statique ou le prérendu, essayez de désactiver JavaScript et chargez la page que vous souhaitez tester. Pour les pages affichées de manière statique, la plupart des fonctionnalités seront disponibles même si JavaScript n'est pas activé. Pour les pages prérendues, il peut encore y avoir certaines fonctionnalités de base telles que les liens, mais la plupart de la page sera inerte.

Un autre test utile consiste à utiliser la limitation du réseau dans les outils pour les développeurs Chrome et à observer la quantité de code JavaScript téléchargé avant qu'une page ne devienne interactive. Le prérendu nécessite généralement plus de JavaScript pour devenir interactif, et JavaScript a tendance à être plus complexe que l'approche d'amélioration progressive utilisée par l'affichage statique.

Rendu côté serveur ou rendu statique

Le rendu côté serveur n'est pas la solution miracle. Sa nature dynamique peut entraîner des coûts de calcul importants. De nombreuses solutions de rendu côté serveur ne vident pas les données de manière anticipée, peuvent retarder le TTFB ou doubler l'envoi des données (par exemple, l'état intégré utilisé par JavaScript sur le client). Dans React, renderToString() peut être lent, car il est synchrone et monothread. Les nouvelles API DOM du serveur React prenant en charge le streaming, qui permettent de recevoir plus rapidement la partie initiale d'une réponse HTML au navigateur alors que le reste est encore généré sur le serveur.

Pour un rendu côté serveur "correct", vous devrez peut-être trouver ou créer une solution de mise en cache des composants, gérer la consommation de mémoire, appliquer des techniques de mémoisation, etc. Généralement, vous traitez/recompilez la même application plusieurs fois : une fois sur le client et une fois sur le serveur. Ce n'est pas parce que le rendu côté serveur peut s'afficher plus tôt que vous avez soudainement moins de travail à faire. Si vous avez beaucoup de travail sur le client après l'arrivée d'une réponse HTML générée par le serveur sur le client, cela peut toujours entraîner une valeur de requête large et un INP plus élevés pour votre site Web.

Le rendu côté serveur produit du code HTML à la demande pour chaque URL, mais il peut être plus lent que de diffuser simplement du contenu statique. Si vous pouvez effectuer des étapes supplémentaires, le rendu côté serveur et la mise en cache HTML peuvent réduire considérablement le délai d'affichage du serveur. L'avantage du rendu côté serveur est la possibilité d'extraire plus de données "en direct" et de répondre à un ensemble de requêtes plus complet qu'avec le rendu statique. Les pages nécessitant une personnalisation sont un exemple concret de ce type de requête qui ne fonctionnerait pas bien avec l'affichage statique.

Le rendu côté serveur peut également prendre des décisions intéressantes lors de la création d'une PWA: est-il préférable d'utiliser la mise en cache pleine page par un service worker ou simplement d'afficher un contenu individuel par le serveur ?

Rendu côté client

L'affichage côté client consiste à afficher les pages directement dans le navigateur avec JavaScript. Toute la logique, l'extraction de données, la modélisation et le routage sont gérés sur le client plutôt que sur le serveur. Le résultat final est que davantage de données sont transmises à l'appareil de l'utilisateur depuis le serveur, ce qui s'accompagne de certains compromis.

L'affichage côté client peut être difficile à obtenir et à maintenir rapide pour les appareils mobiles. Si un travail minimal est effectué, le rendu côté client peut atteindre les performances du rendu pur côté serveur, en conservant un budget JavaScript restreint et en générant de la valeur en le plus peu possible d'aller-retours. Les données et scripts critiques peuvent être livrés plus tôt à l'aide de <link rel=preload>, ce qui permet à l'analyseur de fonctionner plus rapidement. Il est également utile d'évaluer des modèles tels que PRPL pour s'assurer que les navigations initiales et ultérieures sont instantanées.

Schéma illustrant le rendu côté client affectant le FCP et le TTI.

Le principal inconvénient de l'affichage côté client est que la quantité de JavaScript requise augmente à mesure que l'application se développe, ce qui peut avoir des effets négatifs sur l'INP d'une page. Cela devient particulièrement difficile avec l'ajout de nouvelles bibliothèques JavaScript, de polyfills et de code tiers, qui sont en concurrence pour la puissance de traitement et doivent souvent être traités avant que le contenu d'une page puisse être affiché.

Les expériences qui utilisent l'affichage côté client qui reposent sur des bundles JavaScript volumineux doivent envisager une répartition agressive du code afin de réduire la valeur de requête large et l'INP pendant le chargement de la page, et veiller à charger le code JavaScript de façon différée : "ne diffusez que ce dont vous avez besoin, quand vous en avez besoin". Pour les expériences avec peu ou pas d'interactivité, le rendu côté serveur peut représenter une solution plus évolutive à ces problèmes.

Si vous créez des applications monopages, vous pouvez appliquer la technique de mise en cache du shell d'application afin d'identifier les parties essentielles de l'interface utilisateur partagées par la plupart des pages. Combinée aux service workers, cette approche peut considérablement améliorer les performances perçues lors des visites répétées, car le code HTML du shell de l'application et ses dépendances peuvent être chargés très rapidement à partir de CacheStorage.

Combiner le rendu côté serveur et le rendu côté client par réhydratation

Cette approche vise à fluidifier les compromis entre le rendu côté client et le rendu côté serveur en faisant les deux. Les requêtes de navigation, telles que le chargement ou l'actualisation de la page complète, sont gérées par un serveur qui affiche l'application au format HTML, puis le code JavaScript et les données utilisées pour l'affichage sont intégrés dans le document obtenu. Lorsqu'elle est effectuée avec soin, cette méthode permet d'obtenir un FCP rapide, tout comme le rendu côté serveur, puis d'effectuer un nouveau rendu sur le client à l'aide d'une technique appelée (ré)hydratation. Il s'agit d'une solution efficace, mais elle peut présenter des inconvénients considérables en termes de performances.

Le principal inconvénient du rendu côté serveur avec réhydratation est qu'il peut avoir un impact négatif significatif sur la TTC et l'INP, même s'il améliore le FCP. Les pages affichées côté serveur peuvent sembler à tort être chargées et interactives, mais ne peuvent pas répondre aux entrées tant que les scripts côté client pour les composants n'ont pas été exécutés et que les gestionnaires d'événements n'ont pas été associés. Cela peut prendre quelques secondes, voire quelques minutes sur mobile.

Vous avez peut-être rencontré ce problème vous-même. Pendant un certain temps après qu'il semble qu'une page s'est chargée, cliquer ou appuyer n'a aucun effet. Cela devient rapidement frustrant, car l'utilisateur se demande pourquoi rien ne se passe lorsqu'il essaie d'interagir avec la page.

Un problème de réhydratation: une appli au prix de deux

Les problèmes de réhydratation sont souvent plus graves que les problèmes d'interactivité différée dus à JavaScript. Pour que le code JavaScript côté client puisse "reprendre" avec précision là où le serveur s'est arrêté sans avoir à demander de nouveau toutes les données utilisées par le serveur pour afficher son code HTML, les solutions actuelles de rendu côté serveur sérialisent généralement la réponse des dépendances de données d'une interface utilisateur dans le document sous forme de tags de script. Le document HTML obtenu présente un niveau élevé de duplication:

Document HTML contenant une UI sérialisée, des données intégrées et un script bundle.js

Comme vous pouvez le voir, le serveur renvoie une description de l'interface utilisateur de l'application en réponse à une requête de navigation, mais il renvoie également les données sources utilisées pour composer cette UI, ainsi qu'une copie complète de l'implémentation de l'interface utilisateur qui démarre ensuite sur le client. Cette interface utilisateur ne devient interactive qu'une fois le chargement et l'exécution de bundle.js terminés.

Les métriques de performances collectées à partir de sites Web réels à l'aide de l'affichage côté serveur et de la réhydratation indiquent que leur utilisation doit être déconseillée. En fin de compte, tout dépend de l'expérience utilisateur: il est extrêmement facile de laisser les utilisateurs dans une « vallée étrange », où l'interactivité semble absente alors que la page semble être prête.

Schéma illustrant l&#39;impact négatif du rendu client sur le TTI.

Toutefois, il y a de l'espoir pour un rendu côté serveur avec une réhydratation. À court terme, seule l'utilisation du rendu côté serveur pour le contenu pouvant être mis en cache à grande échelle peut réduire le TTFB, produisant des résultats semblables à ceux du prérendu. Une réhydratation incrémentale, progressive ou partielle peut être la solution pour rendre cette technique plus viable à l'avenir.

Rendu en streaming côté serveur et réhydratation progressive

Le rendu côté serveur a connu un certain nombre de développements ces dernières années.

Le rendu côté serveur en flux continu vous permet d'envoyer du code HTML par segments que le navigateur peut afficher progressivement à mesure qu'il est reçu. Cela peut entraîner un FCP rapide, car le balisage arrive aux utilisateurs plus rapidement. Dans React, les flux étant asynchrones dans [renderToPipeableStream()] (par rapport à renderToString() synchrones), la contre-pression est bien gérée.

Une réhydratation progressive est également utile, et React a fait son apparition. Avec cette approche, les éléments individuels d'une application affichée sur le serveur sont "démarrés" au fil du temps, au lieu de l'approche courante actuelle qui consiste à initialiser l'ensemble de l'application en une seule fois. Cela permet de réduire la quantité de code JavaScript nécessaire pour rendre les pages interactives, car la mise à niveau côté client des parties les moins prioritaires de la page peut être différée afin d'éviter de bloquer le thread principal. Ainsi, les interactions utilisateur se produisent plus tôt une fois que l'utilisateur les a initiés.

La réhydratation progressive peut également aider à éviter l'un des pièges de réhydratation de rendu côté serveur les plus courants : une arborescence DOM rendue par le serveur est détruite, puis immédiatement reconstruite, le plus souvent car le rendu synchrone initial côté client nécessitait des données qui n'étaient pas encore tout à fait prêtes, peut-être en attente de résolution d'une Promise.

Réhydratation partielle

La réhydratation partielle s'est révélée difficile à mettre en œuvre. Cette approche est une extension de l'idée de la réhydratation progressive, où les éléments individuels (composants/vues/arbres) à réhydrater progressivement sont analysés et ceux avec peu d'interactivité ou pas de réactivité sont identifiés. Pour chacune de ces parties principalement statiques, le code JavaScript correspondant est ensuite transformé en références inertes et en fonctionnalités décoratives, réduisant ainsi leur empreinte côté client à presque zéro.

L'approche de l'hydratation partielle présente ses propres problèmes et compromis. Cela pose des problèmes intéressants pour la mise en cache, et la navigation côté client signifie que nous ne pouvons pas supposer que le code HTML affiché par le serveur pour les parties inertes de l'application sera disponible sans le chargement complet de la page.

Rendu trisomorphique

Si vous avez la possibilité d'utiliser des service workers, le rendu "trisomorphique" peut également vous intéresser. Cette technique vous permet d'utiliser le rendu côté serveur en streaming pour les navigations initiales/autres que JavaScript, puis de demander à votre service worker d'afficher le code HTML pour les navigations après son installation. Cela permet de maintenir à jour les composants et les modèles mis en cache, et d'activer les navigations de type SPA pour afficher de nouvelles vues dans la même session. Cette approche fonctionne mieux lorsque vous pouvez partager le même code de création de modèles et de routage entre le serveur, la page du client et le service worker.

Schéma du rendu trisomorphique montrant un navigateur et un service worker en communication avec le serveur.

Considérations relatives au SEO

Les équipes tiennent souvent compte de l'impact du SEO lorsqu'elles choisissent une stratégie pour le rendu sur le Web. Le rendu côté serveur est souvent choisi pour offrir une expérience "complète" que les robots d'exploration peuvent interpréter facilement. Les robots d'exploration peuvent comprendre JavaScript, mais il existe souvent des limites à connaître concernant la façon dont ils s'affichent. Le rendu côté client peut fonctionner, mais souvent pas sans tests et étapes supplémentaires. Plus récemment, l'affichage dynamique est également devenu une option si votre architecture dépend fortement du code JavaScript côté client.

En cas de doute, l'outil de test d'optimisation mobile est inestimable pour vérifier que l'approche que vous avez choisie répond à vos attentes. Il offre un aperçu de la façon dont une page apparaît au robot d'exploration Google, du contenu HTML sérialisé trouvé (après l'exécution du script JavaScript) et des erreurs rencontrées lors de l'affichage.

Capture d&#39;écran de l&#39;interface utilisateur du test d&#39;optimisation mobile.

Conclusion

Lorsque vous choisissez une approche pour le rendu, mesurez et identifiez vos goulots d'étranglement. Déterminez si le rendu statique ou côté serveur peut vous permettre d'atteindre cet objectif. Il est normal d'envoyer principalement du code HTML avec un minimum de JavaScript pour une expérience interactive. Voici une infographie pratique montrant le spectre serveur-client:

Infographie montrant l&#39;éventail des options décrites dans cet article.

Crédits

Merci à tous pour leurs avis et leurs sources d'inspiration:

Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson et Sebastian Markbåge