ce site a migré : retrouvez cet article à jour sur braincracking.org
Put quoi ?
Il y a quelques années de ça, Yahoo! a découvert une règle de performance frontend qui a fait date, et qui fait toujours partie des must-have :
Plus tard Google PageSpeed a inclus la même règle. De fait, une inclusion de JavaScript bloque tout rendu ET téléchargement durant le temps de téléchargement du fichier. Dans le cas d'une mauvaise connexion, vous pouvez même rester sur une page blanche alors que la majorité de la page est téléchargée.
Ici les images attendent le JS. Temps body.onload : 3.7s. (voir toute la timeline)
Cette règle est donc réellement à considérer dans le top 10 des modifications à apporter pour qu'un site semble plus réactif. Les développeurs qui ont vu ça ont fait le test de déplacer les balises <script> du <head> pour les mettre tout en bas de page ... pour se rendre compte que la page "cassait" et que des modifications de leurs scripts seraient trop pénibles à exécuter.
Objectif
Mon but était d'accélérer le rendu de la homepage de mon projet actuel pour les visiteurs sans cache. La page mettait plus de 3s avant que le DOM ne s'affiche, alors que le HTML était reçu en moins d'une demie seconde. Ayant la chance d'avoir la même problématique que Facebook (1 page de garde qui n'a que 3 fonctions : se loguer, s'enregistrer et parler du service), j'ai pu reporter le téléchargement de l'intro flash, réduire de moitié le poids total des JS et CSS et accélérer l'envoi des scripts avec du cache côté serveur. 60ko de JS ça reste lourd pour une seule page, et je pourrais faire encore plus spécifique, mais cela demande plus de temps et dans nos contrées ADSLisées le poids n'est plus le nerf de la guerre.
Ces modifs étaient plutôt lourdes et je leur ai consacré presque 2 journées, sans gain sensible, alors que déplacer une inclusion de JS prend une minute. Il me fallait donc tenter quelque chose pour ce fichier JS inclus dans le <head>. Mon seul problème était les <script> inline qui se lançaient et qui dépendaient de ce fichier.
Pourquoi du Javascript inline ?
Avant que vous ne criez au scandale, il faut savoir que ce site a été développé de manière modulaire : écrire des parties indépendantes de site qui peuvent être inclues n'importe où, y compris via XHR (dit AJAX). On est donc bien forcé à un moment de lancer le Javascript associé à un module et si vous regardez le code source, le JS inline ne fait qu'instancier les classes correspondantes aux modules inclus (login, animation, feedback ...). Pour les modules communs, cette partie de JS est dans le fichier principal (dans l'illustration plus haut, c'est celui de 67ko), pour d'autres plus exceptionnels répartis ailleurs sur le site dans des fichiers séparés qui ne sont appelés que lorsque le module est récupéré en AJAX (technique dite du lazy-loading) .
La concaténation (pour moins de requetes HTTP) et le lazy-loading (pour moins de poids initial) s'opposent mais lorsque l'on fait une quasi application web, il faut essayer de les doser pour obtenir le meilleur des 2 mondes. Dans l'un ou l'autre cas, le cache du navigateur rendra l'expérience fluide.
Voilà donc pourquoi on se retrouve à vouloir exécuter le code que l'on voulait justement inclure après.
Exécuter du code qui n'est pas là ?
La technique se déroule en 5 temps :
- taire les erreurs JS
- la page se charge, les JS inline se lancent et plantent en silence
- remettre le système d'erreur standard
- inclure le fichier
- re-exécuter tous les scripts inline
Concrètement, dans le <head>, à la place du <script src=""> original :
<script> window.defaultOnError = window.onerror;
window.onerror = function() {return true;};
</script>
On a sauvegardé le gestionnaire d'erreur par défaut (window.onerror) pour plus tard et on définit à la place une fonction qui ne fait rien. Les erreurs n'arrivent plus jusqu'à l'utilisateur.
En bas de page, après </html> :
<script> window.onerror = window.defaultOnError; </script> <script src="http://example.com/my.js"></script>
On a remis le gestionnaire par défaut et on a inclus notre fichier JS qui ne gène plus personne. Ensuite on va retrouver les script inline pour les exécuter :
<script> var aInlineJS =document.getElementById('container').getElementByTagName('SCRIPT'),
iTotal = aInlineJS.length;
for(var i=0; i < iTotal;i++) {
eval( oJscripts[i].innerHTML );
} </script>
où container serait l'id de ma div principale. Ici on a :
- récupéré tous les éléments de type <script> de la page, qui ne sont pas nos 3 derniers éléments <script> (sinon attention aux boucles infinies)
- lancé un eval sur le contenu de chaque balise
Le vrai code est à peine plus long car il prévoit plus de cas, mais vous avez l'idée. Vous pouvez regarder le code dans la source de cette homepage. Voici notre nouvelle timeline :
Les images sont récupérées en même temps que le JS. Temps body.onload : 1.7s (voir la timeline complète)
Au bout de quelques semaines, Google aussi a la sensation que la page est plus rapide :
On a gagné :
- 2 secondes (50%), sur body.onload (on passe sous la barre arbitraire des 20% des sites les plus rapides choisie par Google webmaster tools)
- 1 seconde (65%) sur le temps avant le 1er clic
- une sensation de vitesse et de fluidité qu'il n'y avait pas avant, car le HTML et quelques images se chargent en moins d'une seconde et il y a moins de pointe de charge CPU
- des places dans le classement Google ? Plusieurs semaines après, en vérifiant notre position pour certains mots clés dans Google nous nous sommes rendu compte qu'on avait gagné des places, parfois plusieurs pages. Difficile d'être sur à 100% que cela vient de là, mais aucun action SEO ou marketing n'avait été mise en place durant cette période.
Génial, je m'y met
Il y a des inconvénients et certaines choses à prendre en compte :
- si il y a une erreur dans vos scripts, votre debuguer vous indiquera comme numéro de ligne celui de l'eval. Je vous conseille donc de réserver cette technique pour la production, et non pour votre environnement de développement.
- document.write() ne marchera pas : il s'exécutera en bas de page ou pire s'exécutera 2 fois. Si vous affichez de la publicité, il est probable que votre régie utilise document.write(). Il n'y a rien à faire à part faire l'appel de la pub tout en bas, puis déplacer la div container au bon endroit, ce qui est excellent pour la perf de manière générale
- si vous comptiez sur JS pour afficher de la publicité ou un widget facebook, cela se fera plus tard qu'actuellement. L'affichage en est d'ailleurs accéléré et se passe sans freeze car le DOM n'est pas modifié alors qu'il est en train de se charger. Mais j'ai déjà vu des commerciaux protester contre ce genre de développement qui accélère l'affichage mais qui rendrait la publicité moins visible. A ce sentiment il vous faut opposer des faits : la performance perçue rapporte réellement de l'argent, plus qu'une publicité qui s'affiche en retard.
- il faut avoir développé préalablement en "non obstrusive javascript", pour que l'utilisateur puisse accéder aux fonctionalités si il clique avant que le JS n'arrive. Exemple : videz le cache et allez cliquer très vite sur le lien "login" en haut à droite. Vous suivrez un lien si JS n'est pas encore là, alors que vous ouvrirez une fenêtre JS avec le même module de login si il est arrivé.
- certains vous diront par réflexe qu'eval is evil, mais en l'occurrence vous exécutez le code d'une source en laquelle vous faîtes déjà confiance : votre HTML. Les risques de XSS ne sont donc pas plus élevés qu'avant (mais corrigez moi si je me trompe, parce que j'ai déjà mis ça en production ...)
- si votre HTML est plus long à s'afficher entièrement que le JS à télécharger (résultat de recherche, page très lourde, très mauvaise optimisation ...), alors il vaut mieux garder votre JS dans le <head>, envoyer rapidement les parties de HTML déjà calculées (au moins la partie <head>, pour commencer ASAP le téléchargement JS/CSS). De cette manière l'utilisateur pourra déjà commencer à interagir avec la page, parties JS comprises, avant même que le HTML soit entièrement calculé
Si vous voyez d'autres limites, j'attend vos commentaires
D'autres techniques ?
Facebook et Google Analytics ont développé des techniques particulières pour exécuter du code alors qu'il n'est pas forcément encore téléchargé.
Google analytics utilise dans sa version asynchrone une astuce tirant parti de la versatilité de JS. Un Array est déclaré et le webmaster utilise .push() en lui passant le nom et les paramètres des méthodes à appeler. Lorsque le fichier ga.js arrive, celui ci remplace le tableau par une classe, exécute toutes les commandes demandées auparavant et .push() sert maintenant à exécuter directement les méthodes.
Facebook pour sa part a expliqué que pour passer de 5s à 2.5s avant le 1er clic, ils ont du faire du très spécifique : ils déclarent en haut de page un petit listener JS, le reste du JS étant en bas de page (145Ko en 16 requêtes). Si l'utilisateur clique vite, ce petit code ouvre une popup JS qui va chercher sur le serveur le HTML et le JS a exécuter.
Dans ces 2 cas, décaler JS en bas de page demandait à changer sa manière de programmer, ce qui fait partie des choses que je voulais éviter. La technique du mute+eval est exactement adaptée à la problématique que j'avais, aussi je serais curieux de voir ses limites sur d'autres types de pages, ou au contraire si elle peut s'adapter chez vous aussi
Qu'en pensez vous ?
Veille techno et articles similaires via RSS, Twitter, Facebook (twitter + blog ou blog seul), identi.ca, Delicious, Mail .
Technique intéressante mais je n'ai jamais eu à travailler avec des scripts inlines donc je ne saurais pas juger de son efficacité. Je préfère encore avoir le moins de scripts possibles et les insérer en tout dernier :)
Rédigé par : Tbassetto | 01/06/2010 à 10:24
faire sans, c'est mieux :)
après il y a toujours une balance à faire entre la performance pure et les coûts de maintenance, ce qui force à inventer un peu :)
Rédigé par : jpvincent | 01/06/2010 à 10:29
On peut noter que le problème de départ se pose aussi si on a utilisé dans le HEAD (HTML4 et support depuis IE4, les enfants… bon ok, avec deux ou trois petits problèmes): les librairies utilisées ne sont pas encore chargées si on souhaite les instancier dans des scripts inline.
Mais je n’ai pas bien compris en quoi avoir des scripts inline était indispensable. Côté serveur, du templating un peu bien foutu (y compris en PHP tout con) devrait permettre d'avoir tous les scripts en bas de page et dans le bon ordre. Tu as un exemple précis où ça ne serait pas possible? Bien sûr il faut un minimum d'organisation, mais ta technique en demande tout autant, si ce n'est plus (le coup de la boucle infinie, notamment, il y a moyen de se planter magistralement suite à une évolution du code). La technique m'apparait plus comme un moyen un peu couteux de patcher une application pas prévue pour ce cas de figure.
Mais je suis une semi-bille en JavaScript donc je dis peut-être des bêtises. :)
Rédigé par : Fvsch | 02/06/2010 à 20:25
ah j'ai peut être mal posé le problème : les scripts inline ne font pas partie de la technique, en fait c'est justement eux qui m'encombraient, puisqu'ils appellaient des scripts (librairies comprises) que je voulais déplacer tout en bas.
Idéalement, il faudrait penser toute application web dès le départ sans une seule ligne de JS inline, mais dans mon cas l'organisation de mon code côté PHP en modules indépendants (qui me donne d'autres avantages) m'obligeait à lancer le JS correspondant lors de l'affichage du module. Donc inline.
donc oui par rapport à ce problème de JS inline et de vouloir mettre les dépendances JS en bas de page, c'est un patch, mais qui justement n'est pas très coûteux.
Rédigé par : jpvincent | 03/06/2010 à 08:18