Rendu de serveur dans un environnement sans serveur

L'auteur du matériel, dont nous publions la traduction, est l'un des fondateurs du projet Webiny - un CMS sans serveur basé sur React, GraphQL et Node.js. Il dit que la prise en charge d'une plate-forme cloud sans serveur multi-locataire est une entreprise qui a des tâches spécifiques. De nombreux articles ont déjà été écrits dans lesquels des technologies standard pour l'optimisation de projets Web sont discutées. Parmi eux, le rendu de serveur, l'utilisation de technologies avancées de développement d'applications Web, diverses façons d'améliorer la création d'applications, et bien plus encore. Cet article, d'une part, est similaire aux autres, et d'autre part, il en diffère. Le fait est qu'il est dédié à l'optimisation de projets s'exécutant dans un environnement sans serveur.



La préparation


Afin de faire des mesures qui aideront à identifier les problèmes du projet, nous utiliserons webpagetest.org . Avec l'aide de cette ressource, nous répondrons aux demandes et collecterons des informations sur le temps d'exécution de diverses opérations. Cela nous permettra de mieux comprendre ce que les utilisateurs voient et ressentent lorsqu'ils travaillent avec le projet.

Nous sommes particulièrement intéressés par l'indicateur «First view», c'est-à-dire combien de temps faut-il pour charger un site à partir d'un utilisateur qui le visite pour la première fois. Il s'agit d'un indicateur très important. Le fait est que le cache du navigateur est capable de masquer de nombreux goulots d'étranglement des projets Web.

Indicateurs reflétant les caractéristiques du chargement du site - identification des problèmes


Jetez un œil au tableau suivant.


Analyse des anciens et nouveaux indicateurs d'un projet web

Ici, l'indicateur le plus important peut être reconnu comme «Time to Start Render» - le temps avant le début du rendu. Si vous regardez attentivement cet indicateur, vous pouvez voir que c'est seulement pour commencer le rendu de la page, dans l'ancienne version du projet, que cela prenait presque 2 secondes. La raison de cela réside dans l'essence même de la Single Page Application (SPA). Pour afficher la page d'une telle application à l'écran, vous devez d'abord charger le volumineux JS-bundle (cette étape de chargement de la page est marquée dans la figure suivante par 1). Ensuite, ce bundle doit être traité dans le thread principal (2). Et seulement après cela, quelque chose peut apparaître dans la fenêtre du navigateur.


(1) Téléchargez le pack JS. (2) En attente du traitement du bundle dans le thread principal

Cependant, ce n'est qu'une partie de l'image. Une fois que le thread principal a traité le bundle JS, il envoie plusieurs requêtes à l'API Gateway. À ce stade du traitement de la page, l'utilisateur voit un indicateur de chargement rotatif. La vue n'est pas des plus agréables. Cependant, l'utilisateur n'a encore vu aucun contenu de page. Voici un storyboard du processus de chargement de page.


Chargement de la page

Tout cela suggère que l'utilisateur qui a visité un tel site ne ressent pas des sensations particulièrement agréables en travaillant avec lui. À savoir, il est obligé de regarder une page vierge pendant 2 secondes, puis une autre seconde - à l'indicateur de téléchargement. Cette seconde est ajoutée au temps de préparation de la page car, après le chargement et le traitement, les demandes d'API JS-bundle sont exécutées. Ces requêtes sont nécessaires pour charger les données et, par conséquent, afficher la page terminée.


Chargement de la page

Si le projet était hébergé sur un VPS normal, le temps nécessaire pour exécuter ces demandes d'API serait généralement prévisible. Cependant, les projets exécutés dans un environnement sans serveur sont affectés par le problème notoire du «démarrage à froid». Dans le cas de la plateforme cloud Webiny, la situation est encore pire. Les fonctionnalités AWS Lambda font partie de VPC (Virtual Private Cloud). Cela signifie que pour chaque nouvelle instance d'une telle fonction, vous devez initialiser ENI (Elastic Network Interface, interface réseau élastique). Cela augmente considérablement le temps de démarrage à froid des fonctions.

Voici quelques délais pour le chargement des fonctionnalités AWS Lambda dans les VPC et en dehors des VPC.


Analyse de charge de la fonction AWS Lambda à l'intérieur du VPC et à l'extérieur du VPC (image prise à partir d'ici )

De cela, nous pouvons conclure que dans le cas où la fonction est lancée à l'intérieur du VPC, cela donne une augmentation de 10 fois le temps de démarrage à froid.

De plus, ici un autre facteur doit être pris en compte - les retards de transmission des données du réseau. Leur durée est déjà incluse au moment où il faut pour exécuter les requêtes API. Les demandes sont lancées par le navigateur. Par conséquent, il s'avère qu'au moment où l'API répond à ces demandes, le temps nécessaire pour envoyer la demande du navigateur à l'API et le temps nécessaire à la réponse pour passer de l'API au navigateur sont ajoutés. Ces retards surviennent lors de chaque demande.

Tâches d'optimisation


Sur la base de l'analyse ci-dessus, nous avons formulé plusieurs tâches que nous devions résoudre pour optimiser le projet. Les voici:

  • Amélioration de la vitesse d'exécution des demandes d'API ou réduction du nombre de demandes d'API qui bloquent le rendu.
  • Réduire la taille du bundle JS ou convertir ce bundle en ressources qui ne sont pas nécessaires pour la sortie de la page.
  • Déverrouillage du fil principal.

Approches à problèmes


Voici quelques approches pour résoudre les problèmes que nous avons envisagés:

  1. Optimisation du code en vue d'accélérer son exécution. Cette approche nécessite beaucoup d'efforts, elle a un coût élevé. Les avantages qui peuvent être obtenus à la suite d'une telle optimisation sont douteux.
  2. Augmentez la quantité de RAM disponible pour les fonctionnalités AWS Lambda. C'est facile à faire, le coût d'une telle solution se situe entre moyen et élevé. Seuls de petits effets positifs peuvent être attendus de l'application de cette solution.
  3. L'utilisation d'une autre façon de résoudre le problème. Certes, à ce moment-là, nous ne savions pas encore quelle était cette méthode.

Au final, nous avons choisi le troisième élément de cette liste. Nous avons raisonné comme ceci: «Et si nous n'avons absolument pas besoin d'appels API? Et si nous pouvions nous passer du bundle JS? Cela nous permettrait de résoudre tous les problèmes du projet. »


La première idée que nous avons trouvée intéressante était de créer un instantané HTML de la page rendue et de partager l'instantané avec les utilisateurs.

Tentative infructueuse


Webiny Cloud est une infrastructure sans serveur basée sur AWS Lambda qui prend en charge les sites Webiny. Notre système peut détecter les bots. Lorsqu'il s'avère que la demande a été effectuée par le bot, cette demande est redirigée vers l'instance Puppeteer , qui rend la page à l'aide de Chrome sans interface utilisateur. Le code HTML prêt à l'emploi de la page est envoyé au bot. Cela a été fait principalement pour des raisons de référencement, car de nombreux robots ne savent pas comment exécuter JavaScript. Nous avons décidé d'utiliser la même approche pour préparer des pages destinées aux utilisateurs ordinaires.


Cette approche fonctionne bien dans les environnements qui ne prennent pas en charge JavaScript. Cependant, si vous essayez de donner des pages pré-rendues à un client dont le navigateur prend en charge JS, la page s'affiche, mais ensuite, après avoir téléchargé les fichiers JS, les composants React ne savent tout simplement pas où les monter. Cela se traduit par un tas de messages d'erreur dans la console. En conséquence, une telle décision ne nous convenait pas.

Présentation de la SSR


Le côté fort du rendu côté serveur (SSR) est que toutes les demandes d'API sont exécutées au sein du réseau local. Puisqu'ils sont traités par un certain système ou fonction qui s'exécute à l'intérieur du VPC, les retards qui se produisent lors de l'exécution des requêtes du navigateur vers le backend de ressource ne sont pas caractéristiques. Bien que dans ce scénario, le problème d'un «démarrage à froid» demeure.

Un avantage supplémentaire de l'utilisation de SSR est que nous donnons au client une telle version HTML de la page, lors de l'utilisation avec laquelle, après le chargement des fichiers JS, les composants React n'ont pas de problèmes de montage.

Et enfin, nous n'avons pas besoin d'un très grand ensemble JS. De plus, nous pouvons nous passer d'appels API pour afficher la page. Un bundle peut être chargé de manière asynchrone et cela ne bloquera pas le thread principal.

En général, nous pouvons dire que le rendu du serveur aurait dû résoudre la plupart de nos problèmes.

Voici à quoi ressemble l'analyse de site après l'application du rendu côté serveur.


Mesures du site après application du rendu du serveur

Désormais, les demandes d'API ne sont pas exécutées et la page peut être consultée avant le chargement du grand ensemble JS. Mais si vous regardez attentivement la première demande, vous pouvez voir qu'il faut presque 2 secondes pour obtenir un document du serveur. Parlons-en.

Problème avec TTFB


Nous discutons ici de la métrique TTFB (Time To First Byte, temps jusqu'au premier octet). Voici les détails de la première demande.


Détails de la première demande

Pour traiter cette première demande, nous devons procéder comme suit: lancer le serveur Node.js, effectuer le rendu du serveur, effectuer des requêtes API et exécuter du code JS, puis renvoyer le résultat final au client. Le problème ici est que tout cela, en moyenne, prend 1-2 secondes.

Notre serveur, qui effectue le rendu du serveur, doit faire tout ce travail et ce n'est qu'après cela qu'il pourra transmettre le premier octet de la réponse au client. Cela conduit au fait que le navigateur a un temps très long pour attendre le début de la réponse à la demande. En conséquence, il s'avère que maintenant, pour la sortie de la page, vous devez produire presque la même quantité de travail qu'auparavant. La seule différence est que ce travail est effectué non pas côté client, mais sur le serveur, dans le processus de rendu du serveur.

Ici, vous pouvez avoir une question sur le mot "serveur". Nous avons toujours parlé du système sans serveur. D'où vient ce «serveur»? Nous avons bien sûr essayé de rendre le rendu du serveur dans les fonctions AWS Lambda. Mais il s'est avéré qu'il s'agit d'un processus très consommateur de ressources (en particulier, il était nécessaire d'augmenter considérablement la quantité de mémoire afin d'obtenir plus de ressources processeur). En outre, le problème du «démarrage à froid», que nous avons déjà mentionné, est également ajouté ici. En conséquence, la solution idéale était alors d'utiliser un serveur Node.js qui chargerait les documents du site et effectuerait leur rendu côté serveur.

Revenons aux conséquences de l'utilisation du rendu côté serveur. Jetez un œil au storyboard suivant. Il est facile de voir qu'il n'est pas particulièrement différent de celui obtenu lors de l'étude du projet, rendu au client.


Chargement de la page lors de l'utilisation du rendu côté serveur

L'utilisateur est obligé de regarder une page vierge pendant 2,5 secondes. C'est triste.

Bien qu'en regardant ces résultats, on pourrait penser que nous n'avons absolument rien obtenu, ce n'est en fait pas le cas. Nous avions un instantané HTML de la page contenant tout ce dont nous avions besoin. Ce cliché était prêt à fonctionner avec React. Dans le même temps, lors du traitement de la page sur le client, il n'était pas nécessaire d'effectuer des requêtes API. Toutes les données nécessaires ont déjà été intégrées dans HTML.

Le seul problème était que la création de cet instantané HTML prenait trop de temps. À ce stade, nous pourrions investir plus de temps dans l'optimisation du rendu du serveur, ou simplement mettre en cache ses résultats et donner aux clients un instantané de la page à partir de quelque chose comme un cache Redis. C'est exactement ce que nous avons fait.

Mise en cache des résultats de rendu du serveur


Après qu'un utilisateur visite le site Web Webiny, nous vérifions tout d'abord le cache Redis centralisé pour voir s'il existe un instantané HTML de la page. Si oui, nous donnons à l'utilisateur une page du cache. En moyenne, cela a abaissé le TTFB à 200-400 ms. C'est après l'introduction du cache que nous avons commencé à remarquer des améliorations significatives dans les performances du projet.


Chargement de la page lors de l'utilisation du rendu et du cache côté serveur

Même l'utilisateur qui visite le site pour la première fois, voit le contenu de la page en moins d'une seconde.

Voyons maintenant à quoi ressemble le diagramme en cascade.


Mesures du site après application du rendu et de la mise en cache côté serveur

La ligne rouge indique un horodatage de 800 ms. C'est là que le contenu de la page est complètement chargé. De plus, ici, vous pouvez voir que les bundles JS sont chargés à environ 1,3 s. Mais cela n'affecte pas le temps dont l'utilisateur a besoin pour voir la page. Dans le même temps, vous n'avez pas besoin d'appeler l'API et de charger le thread principal pour afficher la page.

Faites attention au fait que les indicateurs temporaires concernant le chargement du bundle JS, l'exécution des requêtes API et l'exécution des opérations dans le thread principal jouent toujours un rôle important dans la préparation de la page pour le travail. Cet investissement de temps et de ressources est nécessaire pour que la page devienne «interactive». Mais cela ne joue aucun rôle, d'une part, pour les robots des moteurs de recherche, et d'autre part, pour créer le sentiment de «chargement rapide des pages» parmi les utilisateurs.

Supposons qu'une page soit «dynamique». Par exemple, il affiche un lien dans l'en-tête pour accéder au compte d'utilisateur dans le cas où l'utilisateur qui consulte la page est connecté. Après le rendu côté serveur, la page à usage général sera envoyée au navigateur. C'est-à-dire celui qui est affiché pour les utilisateurs qui ne sont pas connectés. Le titre de cette page changera, reflétant le fait que l'utilisateur s'est connecté, uniquement après le chargement du bundle JS et les appels d'API. Il s'agit ici de l'indicateur TTI (Time To Interactive, time to the first interactivity).

Quelques semaines plus tard, nous avons constaté que notre serveur proxy ne ferme pas la connexion avec le client là où elle est nécessaire, au cas où le rendu du serveur serait lancé en arrière-plan. La correction de littéralement une ligne de code a conduit au fait que l'indicateur TTFB a été réduit au niveau de 50 à 90 ms. En conséquence, le site a commencé à s'afficher dans le navigateur après environ 600 ms.

Cependant, nous avons fait face à un autre problème ...

Problème d'invalidation du cache


"En informatique, il n'y a que deux choses complexes: l'invalidation du cache et la dénomination d'entité."
Phil Carleton

L'invalidation du cache est en effet une tâche très difficile. Comment le résoudre? Premièrement, vous pouvez souvent mettre à jour le cache en définissant un temps de stockage très court pour les objets mis en cache (TTL, Time To Live, durée de vie). Cela entraînera parfois le chargement des pages plus lentement que d'habitude. Deuxièmement, vous pouvez créer un mécanisme d'invalidation du cache basé sur certains événements.

Dans notre cas, ce problème a été résolu en utilisant un très petit TTL de 30 secondes. Mais nous avons également réalisé la possibilité de fournir aux clients des données obsolètes à partir du cache. Au moment où les clients reçoivent ces données, le cache est mis à jour en arrière-plan. Grâce à cela, nous nous sommes débarrassés des problèmes, tels que les retards et le "démarrage à froid", qui sont typiques des fonctions AWS Lambda.

Voici comment cela fonctionne. Un utilisateur visite le site Web Webiny. Nous vérifions le cache HTML. S'il y a une capture d'écran de la page, nous la donnons à l'utilisateur. L'âge d'une photo peut même être de quelques jours. En transmettant cet ancien instantané à l'utilisateur en quelques centaines de millisecondes, nous lançons simultanément la tâche de créer un nouvel instantané et de mettre à jour le cache. Cela prend généralement quelques secondes pour terminer cette tâche, car nous avons créé un mécanisme grâce auquel nous avons toujours un certain nombre de fonctions AWS Lambda qui sont déjà en cours d'exécution et prêtes à fonctionner. Par conséquent, nous n'avons pas à, lors de la création de nouvelles images, passer du temps sur le démarrage à froid des fonctions.

Par conséquent, nous renvoyons toujours les pages du cache aux clients, et lorsque l'âge des données mises en cache atteint 30 secondes, le contenu du cache est mis à jour.

La mise en cache est certainement un domaine dans lequel nous pouvons encore améliorer quelque chose. Par exemple, nous envisageons la possibilité de mettre à jour automatiquement le cache lorsque l'utilisateur publie une page. Cependant, un tel mécanisme de mise à jour du cache n'est pas idéal non plus.

Par exemple, supposons que la page d'accueil d'une ressource affiche les trois articles de blog les plus récents. Si le cache est mis à jour lors de la publication d'une nouvelle page, alors, d'un point de vue technique, seul le cache de cette nouvelle page sera généré après publication. Le cache de la page d'accueil sera obsolète.

Nous recherchons toujours des moyens d'améliorer le système de mise en cache de notre projet. Mais jusqu'à présent, l'accent a été mis sur le tri des problèmes de performances existants. Nous pensons que nous avons fait du bon travail pour résoudre ces problèmes.

Résumé


Au début, nous avons utilisé le rendu côté client. Ensuite, en moyenne, l'utilisateur pouvait voir la page en 3,3 secondes. Maintenant, ce chiffre est tombé à environ 600 ms. Il est également important que nous supprimions maintenant l'indicateur de téléchargement.

Pour atteindre ce résultat, nous avons été autorisés, principalement, à utiliser le rendu serveur. Mais sans un bon système de mise en cache, il s'avère que les calculs sont simplement transférés du client vers le serveur. Et cela conduit au fait que le temps nécessaire à l'utilisateur pour voir la page ne change pas beaucoup.

L'utilisation du rendu serveur a une autre qualité positive, non mentionnée précédemment. Nous parlons du fait que cela facilite la visualisation des pages sur les appareils mobiles faibles. La vitesse de préparation d'une page à visualiser sur de tels appareils dépend des modestes capacités de leurs processeurs. Le rendu du serveur vous permet d'en supprimer une partie de la charge. Il convient de noter que nous n'avons pas mené d'étude spéciale sur ce problème, mais le système dont nous disposons devrait permettre d'améliorer la visualisation du site sur les téléphones et les tablettes.

En général, nous pouvons dire que la mise en œuvre du rendu de serveur n'est pas une tâche facile. Et le fait que nous utilisons un environnement sans serveur ne fait que compliquer cette tâche. La solution à nos problèmes nécessitait des changements de code, une infrastructure supplémentaire. Nous devions créer un mécanisme de mise en cache bien conçu. Mais en retour, nous avons eu beaucoup de bien. La chose la plus importante est que les pages de notre site se chargent et se préparent au travail beaucoup plus rapidement qu'auparavant. Nous pensons que nos utilisateurs l'apprécieront.

Chers lecteurs! Utilisez-vous des technologies de mise en cache et de rendu serveur pour optimiser vos projets?

Source: https://habr.com/ru/post/fr459306/


All Articles