Depuis sa version 2.0 ASP.NET Core permet de faire un prérendu coté serveur du HTML généré par javascript dans une SPA. J’ai récement essayé d’utiliser cette fonctionnalité sur une application reactjs et ASP.NET Core.
Le principe du rendu coté serveur, ou server-side rendering (SSR) est d’utiliser nodejs pour générer le HTML de l’application coté serveur avant de l’envoyer au client. Cela permet au framework javascript coté client de ne pas avoir besoin de générer le HTML initial, mais simplement d’avoir à attacher les event listeners au DOM. Le rendu de la page au premier affichage est ainsi plus rapide dans une majorité de cas.
Un autre avantage de faire ce prérendu coté serveur est que l’application peut fonctionner même avec javascript désactivé. Cela peut sembler étrange d’utiliser un framework javascript pour faire une application et d’essayer de supporter les navigateurs sans javascript, mais il a plusieurs cas où ça peut être utile pour des utilisateurs de l’application.
Il y a évidemment le cas où le navigateur de l’utilisateur a javascript de désactivé. Cela est rare, mais il y a des utilisateurs et des compagnies qui le font pour des raisons de sécurité.
Mais ce n’est pas le seul cas, le javascript de l’application peut ne pas fonctionner sur le navigateur de l’utilisateur sans avoir été désactivé. Par exemple s’il y a une erreur de parsing du javascript à cause d’une erreur de programmation ou parce qu’on utilise une fonctionnalité du langage trop récente pour le navigateur. Ou parce que le javascript ne s’est pas entièrement téléchargé à cause d’une coupure réseau ou parce qu’un CDN est down.
Lorsqu’on fait une application qui fonctionne sans javascript, on n’a pas à se poser la question de quel navigateur on veut supporter dans notre application.
Mais la raison principale pour laquelle je me suis intéressé au sujet c’est pour le référencement par les moteurs de recherches.
Les robots des moteurs de recherches naviguent sur un site web et parsent le HTML de ses pages pour l’indexer. S’il n’y a pas de HTML dans la page, il n’y a rien à indexer.
C’est vrai que le robot Google va maintenant faire un rendu du javascript pour indexer les SPA. Mais il ne fait ça que sous certaines conditions et avec une priorité plus faible que les sites classiques. Et google n’est pas le seul moteur de recherche (il parait)
Si on veut qu’une SPA soit bien référencée par les moteurs de recherche, il est donc indispensable d’avoir un prérendu coté serveur.
Pour faire un prérendu coté serveur avec ASP.NET Core, il faut ajouter le package nuget Microsoft.AspNetCore.SpaServices
Install-Package Microsoft.AspNetCore.SpaServices
Ce package va nous permettre d’utiliser nodejs pour faire le rendu de notre javascript coté serveur.
On a besoin également du package npm aspnet-prerendering
npm install –save aspnet-prerendering
L’étape suivante est d’écrire la fonction javascript qui permettra de faire ce prérendu,
export default createServerRenderer(params => {
return new Promise<RenderResult>((resolve, reject) => {
resolve({
html: ReactDOMServer.renderToString(<App />),
})
});
});
Il faut ensuite inclure ce prérendu généré par cette fonction dans une vue. Pour cela on a besoin d’inclure les tags helpers du package en ajoutant dans _ViewImport.cshtml :
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
On peut ensuite dans la vue de notre SPA utiliser ces helpers pour invoquer le script permettant de faire le prérendu :
<app id="root" asp-prerender-module="wwwroot/ssr.js">Chargement...</app>
Et voila le serveur fait un prérendu de mon application reactjs.
Vu que le rendu est fait coté serveur, il n'est pas nécessaire de le refaire coté client. Dans ce cas, on peut modifier le script faisant le rendu coté client pour ne faire qu’associer les events listener grâce à la methode hydrate de reactDOM :
ReactDOM.hydrate(<App />, document.getElementById('root'));
Et voila mon application React utilise maintenant du prérendu coté serveur
Simple non ?
Malheureusement ce n’est pas si simple parce que mon application possède plusieurs pages, et affiche des données dynamiques.
Mon application est une SPA, elle utilise donc un router pour gérer la navigation de l’utilisateur. Comme son nom l’indique, le BrowserRouter de react-dom-router est fait pour être utilisé dans un navigateur, et ne marche donc pas coté serveur.
Reactjs propose le StaticRouter pour être utilisé coté serveur, j’ai donc besoin d’extraire le router de mon app pour pouvoir utiliser le StaticRouter lors du prérendu coté serveur :
export default createServerRenderer(params => {
return new Promise<RenderResult>((resolve, reject) => {
resolve({
html: ReactDOMServer.renderToString((<StaticRouter location={params.url}><App/></StaticRouter>))
})
});
});
Et utiliser le BrowserRouter coté client :
ReactDOM.hydrate(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'));
Maintenant le serveur pourra faire le prérendu du bon composant react en fonction de l’url de la requête.
Afficher la bonne page c’est bien, mais il faudrait que la page affiche des données. Le problème est que ces données proviennent d’un webservice, et le rendu coté serveur renvoi le résultat du 1er rendu, sans attendre la réponse des webservices.
ASP.NET Core permet de passer en paramètre des données initiales utilisées pour le prérendu coté serveur.
J’ai besoin de modifier mon application react pour qu’elle accepte en entrée des données initiales à afficher, et Il faut que coté serveur je récupère les données à afficher et que je les passe à l’application via la fonction de prérendu. Sauf que mon application utilise un router et possède plusieurs pages affichants chacune des données différentes, il faut donc que coté serveur j’analyse l’url pour aller récupérer les données à afficher en fonction de la page demandée.
Dans le controller j’ajoute :
Match match = Regex.Match(Request.Path.ToString(), @"/blog/\d{4}/\d{1,2}/(.+)");
if (match.Success)
{
string articleUrl = match.Groups[1].Value;
var article = await _articleService.GetArticleByUrl(articleUrl);
ViewData["article"] = article;
}
match = Regex.Match(Request.Path.ToString(), @"/tag/(.+)");
if (match.Success)
{
string tag = match.Groups[1].Value;
var articles = await _articleService.GetArticlesWithTag(tag);
ViewData["article_list] = article_list;
}
…
En gros faut que je recode la logique du router dans le controller. Les données initiales à afficher sont passées à la vue par les ViewData (la structure de ces données pouvant changer, je préfère ne pas créer de model pour les passer à la vue)
Et dans la vue, je peux utiliser le tag helper pour passer ces données à ma fonction de prerendu
<app id="root" asp-prerender-module="wwwroot/ssr.js" asp-prerender-data="@ViewData">Chargement...</app>
Et je récupère ces données dans la fonction de prérendu coté serveur pour les passer à mon application
export default createServerRenderer(params => {
return new Promise<RenderResult>((resolve, reject) => {
resolve({
html: ReactDOMServer.renderToString((<StaticRouter location={params.url}><App initialData={params.data} /></StaticRouter>)),
globals: {
initalData: params.data
}
})
});
});
La propriété globals permet de passer des données du serveur au client pour que lorsque le rendu est fait coté client ce soit les mêmes données qui soient utilisées. Ces données sont injectées dans la variable window.
Ce qui donne coté client :
ReactDOM.hydrate(<BrowserRouter><App initialData={window["initalData"]} /></BrowserRouter>, document.getElementById('root'));
Il reste un problème à régler pour faire marcher ce prérendu coté serveur, il s’agit de l’utilisation des variables globales du navigateur.
Le rendu coté serveur ne s’exécutant pas dans un navigateur, il est logique que les variables window, navigator, document, … ne soient pas accessibles. J’ai eu besoin de modifier mon application pour ne plus utiliser ces variables lors du rendu coté serveur.
En pratique il y a eu beaucoup plus de chose à modifier que ce que je pensais. Même si je n’utilisais que très peu la variable window, j’utilisais des librairies qui le faisait (par exemple une lib de gestion d’authent AAD),
J’utilisais la variable document pour modifier le titre en fonction de la page affiché. J’ai dû répliquer cette logique dans le controller pour que la page rendu coté serveur ait également le bon titre.
Pour corriger les erreurs liées à ces variables du navigateur, il faut tester l’existence de la variable avant de l’utiliser :
if (typeof window !== 'undefined') {
window.addEventListener('beforeinstallprompt', (e) => …);
}
Ou ne l’utiliser qu’une fois le 1er rendu fait
ComponentDidMount() {
document.title = "..."
}
Mettre en place un prérendu coté serveur a demandé plus de travail que ce que je pensais. Est-ce que ça vallait vraiment le coup.
Avant la page de le l’application faisait 713 octets (avec compression gzip), la page d’accueil fait maintenant 4.8ko
C’est une augmentation qui semble assez importante, mais 4ko de différence ne devrait pas avoir beaucoup d’impact sur le temps de téléchargement de la page.
Par contre le serveur met maintenant plus de 1s pour répondre là ou avant il renvoyait la page en 20ms. Le temps de rendu est donc pas négligeable. Mais avec une mise en cache coté serveur j’arrive à ramener ce temps à peut près à ce qu’il était avant.
J’ai fait un audit de performances avec lighthouse avant et après la mise en place du prérendu serveur.
avant le prérendu coté serveur
avec le prérendu coté serveur
On voit que le temps d’affichage est passé de 6sec à 1.7 sec. Pour un temps de chargement complet légèrement plus élevé avec le prérendu serveur. Cette augmentation du temps de chargement provient probablement du temps de réponse plus élevé lors du téléchargement de la page.
Mais d’un point de vu expérience utilisateur le net gain en temps d’affichage est appréciable.
Mais comme je l’avais dit la raison pour laquelle je me suis lancé sur le prérendu coté serveur c’était pour améliorer le référencement de l’application.
Avant dans la search console de google je pouvais voir que le robot ne voyait qu’une page blanche
Maintenant le robot arrive bien à lire le contenu de la page.
Le prérendu coté serveur à certains désavantages, mais le gain en vitesse d’affichage et en référencement valent le coup.
Commentaires :
Aucun commentaires pour le moment
Laissez un commentaire :