Une nouvelle génération de framework front
Depuis un an environ, nous voyons émerger de nouveaux frameworks front axés sur la performance, ils proposent une alternative à l'hydration traditionnelle.
Développement front
Quand j’ai commencé comme développeur, le développement front-end n’existait pas vraiment, du moins pas comme aujourd’hui. On avait d’un côté des intégrateurs qui se chargeaient de retranscrire en HTML/CSS les maquettes fournies par les équipes UI. De l’autre côté, on avait des développeurs back qui récupéraient le code HTML des intégrateurs pour rendre le contenu dynamique à l’aide de leurs outils de templating. À cette époque, le Javascript n’était que peu utilisé car son intérêt etait assez limité et il était compliqué de produire du code compatible sur tous les navigateurs (Rip IE 🎉).
En 2006, JQuery a vu le jour et a rapidement été adopté par les développeurs, il permettait d’effectuer de la manipulation de DOM facilement et sans se soucier de la compatibilité des navigateurs. Il a contribué à la généralisation des calls Ajax, indispensables aujourd’hui. Cependant bien que très pratique pour implémenter quelques interactions il reste assez limité.
C’est à partir de 2009 qu’on a commencé à voir les premiers vrais frameworks front-end tels qu’on les connait aujourd’hui, Angular fût le premier, puis React, Vue, etc … ont pris le pas. Leur paradigme est complètement différent, la philosophie de ces frameworks repose sur le fait de générer des interfaces UI dynamiques et réactives en se basant sur une approche modulaire dite “composant”. Avant on écrivait du HTML et on le modifiait ou on ajoutait des intéractions avec du JS. Maintenant on écrit du Javascript qui se charge de générer du HTML dynamique et réactifs aux données, aux états et aux interactions utilisateur.
Server side rendering
React, Vue, Svelte, Angular, sont tous des frameworks optimisés pour la création de Single Page Application, c’est-à-dire que le HTML, le contenu de la page, est généré côté client, dans le navigateur de l’utilisateur.
Ce qui pose quelques problèmes notamment pour les applications ayant de fortes contraintes SEO ou de performance.
Pour cela, chacun de ces frameworks propose des utilitaires pour pouvoir faire du Server Side Rendering (SSR)
.
Comment ça marche ?
Concrétement le SSR va permettre de prerender
l’application côté server, ce qui permettra à l’utilisateur (et aux crawlers) de voir du contenu sans devoir attendre que le code Javascript soit chargé et exécuté. Plus synthétiquement, on va générer un HTML côté serveur.
Une fois la page affichée, les JS vont s’exécuter et “hydrater” le contenu. Cette hydration
va permettre au code client
de s’attacher sur les noeuds du DOM générés côté serveur pour y ajouter les interactions. Si on devait schématiser, on peut imaginer que l’application sera rendue une deuxième fois côté client et va venir s’attacher à celle rendu côté serveur.
Quant aux données qui ont été récupérées côté serveur, elles seront sérialisées dans le HTML sous forme d’un objet attaché au window que le client pourra récupérer au chargement pour obtenir le même état que côté serveur.
<script>
window.__DATA__=${JSON.stringify(initialState)};
</script>
Server side generation
Il existe une variante du Server Side Rendering
appelée Server Side Generation (SSG)
qui consiste à utiliser cette fonction de rendering server pour générer des statics HTML plutôt que de les générer à la volée côté server.
Cette méthode permet d’économiser le temps de rendu côté server, en revanche elle est adaptée à des contenus ayant une fréquence de mise à jour moindre.
Le SSG conviendra parfaitement dans le cadre d’un site vitrine, d’un blog ou d’un site édito. En revanche pour un site e-commerce il ne sera pas possible d’utiliser du SSG, il faudra faire du SSR.
Le processus d’hydration
sera similaire entre du SSR et du SSG.
Quelles sont les limites de l’hydration ?
Le SSR nous permet d’obtenir un rendu visuel plus rapidement et d’offrir à l’utilisateur une sensation de rapidité. Cependant, le navigateur n’aura pas moins de Javascript à télécharger que si le rendu était effectué côté client car l’entièreté de la page sera “hydratée”. En effet pour que la page soit interactive, le framework aura besoin de rendre entièrement l’application pour attacher les événements Javascript aux différents éléments. Dans ce sens, plus l’application va grossir, plus le browser aura de Javascript à télécharger et plus sa performance sera impactée. Si vous vous êtes déjà amusés à analyser les sites e-commerce sur lighthouse, vous avez sans doute remarqué que 90% d’entre eux ont de mauvaises notes, en voici la raison, Elles chargent beaucoup de Javascript non utilisé.
Qwik, Astro, Marko, etc… une alternative à l’hydratation SSR ?
Comme nous l’avons vu précédemment, ce procédé d’hydration
totale nous oblige à devoir charger beaucoup de JS, dont beaucoup de JS inutilisé.
C’est quand même dommage de devoir charger du Javascript pour des composants qui sont parfois juste “graphiques” (sans interactions et donc déjà rendues par le serveur) ou pour lesquels on pourrait le télécharger qu’au besoin (au moment de l’interaction).
C’est d’ailleurs ce qui est toujours pointé du doigt lorsqu’on fait un test lighthouse Reduce unused Javascript
. Lors de la WW Conférence 2022, Misko Hevery soulignait d’ailleurs que c’était l’effort principal à mener pour augmenter la note de performance et que les optimisations d’images ou de CSS étaient quasi inutiles sans optimisations du Javascript.
Progressive hydration ou Island hydration
Astro, Marko et Solid se sont positionnés sur ce segment et offrent la possibilité d’hydrater les composants de manière plus fine en choisissant de les hydrater ou non, ou de les hydrater selon certaines conditions (quand le composant est visible, quand le browser est idle ou selon un media query par exemple).
Prenons l’exemple d’Astro, par défaut Astro n’hydratera aucun composant, il se positionne comme un frameork HTML First.
Pour hydrater un composant, il faudra lui passer une propriété client:load
, client:visible
, client:idle
, client:media={QUERY}
, client:only={FRAMEWORK}
:
Exemple avec Astro :
---
// Example: hydrating framework components in the browser.
import InteractiveButton from '../components/InteractiveButton.jsx'
import InteractiveCounter from '../components/InteractiveCounter.vue'
---
<!-- This component's JS will begin importing when the page loads -->
<InteractiveButton client:load />
<!-- This component's Vue will not be sent to the client until
the user scrolls down and the component is visible on the page -->
<InteractiveCounter client:visible />
Vous remarquerez sur l’exemple ci-dessus, que notre code Astro
importe un composant Vue et un composant au format jsx
, en effet Astro permet d’être utilisé avec le framework front de notre choix.
Ils ont développé de nombreux adaptaters
qui permettent d’exécuter des composants vue
, react
, solid
, svelte
etc…
C’est très ingénieux et pourraient faciliter la migration d’applications existantes vers ce framework.
L’hydration progressive permet de limiter beaucoup de Javascript à télécharger et d’obtenir un gain considérable de performance.
Cependant, elle demande de connaître à l’avance, quels sont les composants qui auront besoin ou non d’interactions.
C’est pour cette raison que Qwik et la dernière version de Marko ont imaginé un nouveau concept appelé Resumability
Resumability
Afin de diminuer le Javascript à télécharger, Marko et Qwik vont distinguer les composants qui seront interactifs et ceux qui ne le sont pas. Ceux qui ne le sont pas seront rendus une seule fois côté serveur et n’auront donc pas de Javascript associé. Pour le reste des composants, ils vont faire travailler le serveur et vont aller un peu plus loin dans la sérialisation vue précédemment en sérialisant également les actions à exécuter, les listeners, etc… Autrement dit le HTML rendu par le seveur se suffit à lui-même. Au chargement de la page, si on observe le network, on ne verra aucun JS téléchargé, seules les actions utilisateurs provoqueront le téléchargement des fichiers Javascript 🔥, c’est le principe du Lazy-Loading, on télécharge les ressources quand on en a besoin.
Qwik ne parle pas d’hydration mais il utilise le terme de “reprise”, l’application côté client n’a pas à s’exécuter, elle reprend là où le serveur s’est arrêté.
Voici un exemple d’un simple compteur reprit du playground disponible sur le site de Qwik :
import { component$, useSignal } from '@builder.io/qwik'
export default component$(() => {
const count = useSignal(0)
return (
<div>
<p>Count: {count.value}</p>
<p>
<button onClick$={() => count.value++}>Click</button>
</p>
</div>
)
})
Le HTML rendu par le serveur ressemblera à cela :
<!--qv q:id=0 q:key=Ncbm:0t_0-->
<div q:key="4e_0">
<p>
Count:
<!--t=1-->0<!---->
</p>
<p>
<button
on:click="app_component_div_p_button_onclick_j1hnbkkrneo.js#app_component_div_p_button_onClick_J1HnbkKRNeo[0]"
q:id="2"
>
Click
</button>
</p>
</div>
<!--/qv-->
</body>
<!--/qv-->
<script type="qwik/json">
{"refs":{"2":"0"},"ctx":{},"objs":["\u00121",0,"\u00110 @0","#1"],"subs":[["3 #1 2 #1"]]}
</script>
<script q:func="qwik/json">
document.currentScript.qFuncs=[(p0)=>(p0.value)]
</script>
<script id="qwikloader">
...
</script>
<script>
window.qwikevents.push("click")
</script>
On peut d’abord remarquer des marqueurs sous formes de commentaires qui permettent à Qwik de localiser les composants.
On peut également remarquer que mon event click a été compilé en on:click
faisant référence à un fichier JS qui sera téléchargé au click sur le bouton. La seconde partie de ce marqueur correspond à l’export à exécuter dans ce fichier.
Afin d’éviter les ralentissements dus au téléchargement du fichier au moment d’interaction, Qwik va se charger de précharger le fichier via un service worker ce qui permettra à l’interaction de s’exécuter instantanément.
Dans la partie basse du code, au format JSON, sera stocké un “résumé” de l’état de l’application, notamment l’état de la valeur count
ainsi que son symbol afin de faire le lien avec le signal correspondant.
Une partie importante est le script nommé qwikloader
permet de jouer le rôle d’un global Listener, il écoutera en global les évents qui sont utilisés au travers de l’application (si un événement click a été ajouté dans l’application, Qwik placera un événement global click sur l’application). Ces événements seront sauvegardés dans un objet rattaché au window par l’intermédiaire du script que vous voyez en dernière position window.qwikevents.push('click')
. Avec ces informations, dans notre cas, le qwikloader
poura ajouter un événement click
global qui sera intercepté et à partir duquel, en analysant l’événement, Qwik pourra retrouver l’élément cliqué et ainsi retrouver les informations qui lui sont attachées en lisant les attributs qui ont été ajoutés à la compilation. Dans notre cas, il pourra alors télécharger et exécuter le fichier Javascript correspondant afin de jouer l’action au click sur notre bouton.
À l’aide de cette méthode Qwik est capable de charger la page d’une application réactive complexe avec moins de 1 kb de Javascript ! Notamment car grâce il n’a pas besoin de rééexecuter toute l’application côté client pour attacher les événements javascript aux noeuds du DOM.
Conclusion
J’apprécie tout particulièrement ces nouveaux paradigmes. La web perf est aujourd’hui cruciale pour une marque qui souhaite se démarquer de sa concurrence, c’est notamment un critère important pour le SEO et plus directement, des études démontrent qu’on peut même prendre par un raccourci simple “plus le site est rapide, plus il génère de chiffre d’affaires”, l’utilisateur arrivera plus rapidement au panier, plus le site est lent plus le taux de rebond est élevé. Ces nouveaux frameworks permettent d’atteindre facilement 100% sur lighthouse tout en ne dégradant pas l’expérience utilisateur. Est-ce que cela annonce la fin de React ? Bien sur que non, les usages restent différents. Qwik, Marko et Astro se positionnent sur le segment des applicactions SSR, si vous avez besoin de construire une SPA, l’utilisation de React, Vue, Svelte, etc restera plus adaptée.
Si vous voulez en savoir plus sur ces frameworks, c’est par ici :)