Cet article étudie, dans le cas des fenêtres pop-up, comment on peut éviter le rechargement de code javascript en le partageant d’une fenêtre à l’autre. Cet article fait partie d’une série de plusieurs articles.
Plan de l’article
Chargement du frameworkpour chaque page
Étape 1 : chargement synchrone du framework
Mise en œuvre
Dans cette étape, la première page de l’application, page1.html charge explicitement le framework. Le chargement du fichier skynetlib.js est fait inline via une balise SCRIPT :
<SCRIPT src="skynetlib.js"></SCRIPT>
La balise SCRIPT respecte l’ordonnancement, donc il suffit de lancer la méthode init après le chargement du fichier skynetlib.js :
<SCRIPT src="skynetlib.js"></SCRIPT> <SCRIPT> function init(){ // Création d’une instance de Probe et affectation à une variable var a=new Skynet.tools.Probe(1337) ; } ; init() ; </SCRIPT>
Mesures
Nos mesures sont faites en javascript. Les chiffres annoncés sont des moyennes.
début de l’interprétation du fichier skynetlib.js | 162ms |
début de l’évaluation de la méthode init | 1221ms |
Lancement de l’événement onload | 1280ms |
Noter que l’interprétation du fichier skynetlib.js s’arrête à 1168ms, cette interprétation dure bien de l’ordre de la seconde.
Constat
La méthode init est lancée après chargement de skynetlib.js et ce chargement prend une seconde. La page ne peut donc utiliser le framework qu’après ce temps de chargement.
On voit que le onload de la page est aussi en attente du chargement du fichier javascript. C’est visible sur cette copie d’écran de firebug :
Étape 2 : chargement asynchrone du framework
Mise en œuvre
On sait (cf. par exemple les transparents « Even Faster Web Sites » de Steve Souders) qu’il vaut mieux charger les fichiers javascript en asynchrone qu’en inline.
Dans cette étape, on veut que la première page, page1.html charge dynamiquement le fichier javascript. C’est assez facile à faire, par exemple via le DOM par création d’une balise SCRIPT dans le HEAD :
function loadScript(filename){ var targetTag=document.getElementsByTagName("head")[0]; var nodeS=document.createElement("SCRIPT"); nodeS.src=filename; targetTag.appendChild(nodeS); }
Néanmoins, ce chargement dynamique ne permet pas de garantir l’ordre d’exécution des scripts. Ainsi même si l’appel de loadScript est effectué AVANT l’appel de la méthode init() ; on ne peut pas garantir que le code du framework aura été interprété avant cet appel.
La méthode init() échoue donc puisqu’au moment où elle est appelée, elle ne peut pas accéder au code du framework (la variable Skynet).
On pourra se référer aux transparents de Steve Souders pour les différents moyens d’ordonnancer des scripts chargés dynamiquement. En ce qui me concerne, j’ai directement pris la librairie EFWS qu’il utilise dans son livre « Even faster Web Sites ».
Après l’initialisation de cette librairie (150 lignes en mode non minifié), il suffit de faire
EFWS.Script.loadScripts(["skynetlib.js"], init);
pour que le fichier skynetlib.js soit chargé dynamiquement puis que la méthode init soit appelée.
Attention, ce chargement dynamique garantit que la méthode init est appelée après que le chargement du fichier skynetlib.js,. Il n’y a par contre aucune garantie rien vis-à-vis de l’exécution du onload de la page (qui peut avoir lieu avant ou après).
Mesures
début de l’interprétation du fichier skynetlib.js | 209ms |
début de l’évaluation de la méthode init | 1266ms |
Lancement de l’événement onload | 107ms |
Le chargement du fichier skynetlib commence à 209ms (donc un peu plus tard que le chargement par balise <SCRIPT> de l’étape précédente) et s’arrête à 1221ms.
Constat
L’utilisation d’un chargement dynamique se traduit dans notre exemple par un léger surcoût (l’évaluation de la fonction init commence à 1266ms> à la place de 1221ms). Il est clair cependant que la page d’exemple n’est pas significative : elle ne comporte pratiquement pas de balises HTML, or le chargement dynamique est surtout intéressant parce qu’il ne bloque pas l’évaluation de la page. On voit néanmoins que le onload lors d’un chargement asynchrone :
est lancé plus d’une seconde (1280 – 107) avant celui de la solution synchrone (étape 1).
Partage du framework avec une page pop-up
Nous allons maintenant comparer le temps de chargement d’une fenêtre si au lieu d’être ouverte directement par son url, elle est ouverte en javascript par une page parente.
Rappels
Affichage d’une page en HTML
En html, il suffit d’utiliser un hyperlien pour afficher une page.
Si on ne précise pas l’attribut target, la page courante est remplacée par la page cible :
<A href="Page1.html">Page1.html</A>
Si on précise l’attribut target à la valeur _blank, on ouvre une nouvelle fenêtre. On peut aussi préciser comme target le name d’une frame ou d’une window existante.
<A href="Page1.html" target="_blank">Page1.html</A>
Affichage d’une page en javascript
Il existe plusieurs méthodes en javascript pour afficher une page dans fenêtre.
La première méthode est d’utiliser document.location= »pageCible.html » qui remplace la page courante par pageCible.html.
La seconde méthode utilise window.open. Cette méthode est plus puissante : elle permet d’ouvrir d’autres fenêtres (et pas seulement de remplacer le contenu de la fenêtre courante).
La méthode window.open a la syntaxe suivante :
window.open(url, windowName, windowFeatures)
- url est l’url de la page à ouvrir,
- windowName est le nom de la fenêtre (ce nom peut être utilisé pour rouvrir une nouvelle page dans la même fenêtre)
- windowFeatures, une chaîne avec différentes propriétés que l’on peut donner à la nouvelle fenêtre.
Cela donne par exemple :
window.open("eventEditionPage.html","Event editor",
"location=1,status=0,scrollbars=1, width=100,height=100")
Sur Chrome comme Firefox, si on veut ouvrir la nouvelle page dans une fenêtre qui lui est propre, il suffit de mettre une chaîne non vide dans windowFeatures, sinon, la fenêtre s’ouvre dans un onglet du navigateur.
Si l’on souhaite remplacer la fenêtre courante, il suffit d’utiliser comme windowName le nom de la fenêtre courante. On se retrouve alors avec un comportement identique à l’affectation de document.location.
Cas particulier : le chargement dans la même fenêtre
Il est possible de réutiliser la même fenêtre pour plusieurs fenêtres. soit par balise A avec pour target _self, soit par affectation de document.location, soit par l’utilisation de window.open en précisant pour windowName le même nom de fenêtre que pour la première page.
Même dans ce cas où l’on réutilise la même fenêtre, par exemple si la page page1.html charge la page un page2.html, la page page2.html possède toujours un window.opener. Par contre, on a window.opener==window, et page1.html est inaccessible. Il est donc impossible de récupérer les variables initialisées dans page1.html. Dans ce cas, il est obligatoire de charger skynetlib.js dans page2.html. La seule présence d’un window.opener ne suffit donc pas à garantir que le framework est présent dans ce window.opener.
Autrement dit, on ne peut pas partager un code javascript situé dans la même fenêtre et chargé par une page que la page courante a remplacé.
Mise en oeuvre
Dans la suite de l’exemple, nous introduisons une page master1.html. Cette page charge elle-même le fichier skynetlib.js (en synchrone via une balise SCRIPT).
C’est master1.html qui ouvre la fenêtre pop-up par un appel javascript tel que :
window.open("mapage.html")
Étape 3 : la fenêtre pop-up charge le framework
Mise en œuvre
La page master1.html lance la page page1-frm.html en tant que fenêtre pop-up. La page1-frm fait elle-même un chargement synchrone du fichier skynetlib.js.
Mesures
Au niveau de page1-frm.html (les temps sont mesurés à partir du chargement de cette page) :
début de l’interprétation du fichier skynetlib.js | 175ms |
début de l’évaluation de la méthode init | 1146ms |
Lancement de l’événement onload | 1176ms |
Noter que comme on peut s’y attendre l’utilisation de la méthode de chargement dynamique du framework (avec la page1-dyn.html) ne change que la date de d’événement onload qui se déclenche vers 42ms (comme dans l’étape 2).
Constat
Avec cette méthode, on a chargé deux fois le fichier skynetlib.js : une fois dans la page master1.html, une autre fois dans la page page1-frm.html.
Autrement dit, à chaque fois qu’on ouvre une fenêtre pop-up, on est obligé de recharger le framework dans cette fenêtre pop-up.
Ainsi, si vous avez un composant comme un calendrier et que vous utilisez une page pop-up pour créer ou modifier un événement de calendrier, vous êtes obligés de recharger le framework dans cette fenêtre pop-up. Si vous avez une vue détail de cet page pop-up, par exemple pour pouvoir choisir le fuseau horaire, vous êtes là-aussi obligés de charger le framework pour la vue détail.
Étape 4 : window.open avec partage de framework
On va maintenant chercher à ne charger qu’une seule fois le fichier skynetlib.js. la page master1.html ouvre maintenant page1.html en tant que fenêtre pop-up.
Mise en œuvre
Quand on ouvre une fenêtre via javascript (via document.location ou via window.open), elle comporte une variable window.opener qui pointe sur la window qui l’a ouverte. Dans notre cas, le window.opener de page1.html pointe sur la window de master1.html. Or master1.html a chargé le fichier skynetlib.js.
Il suffit donc de faire Skynet=window.opener.Skynet dans page1.html pour que le framework soit accessible directement, sans aucune modification du code utilisant le framework.
Noter que cela nécessite d’être sur un site web, pour des questions de sécurité (liées au cross scripting), il n’est pas possible d’utiliser le protocole file://.
Ainsi, après cette affectation, dans page2.html, le code de la fonction init :
var a=new Skynet.tools.Probe(1337) ;
fonctionne parfaitement sans chargement du fichier skynetlib.js.
Remarque
Noter que si on ferme la fenêtre parente (alors que la fenêtre pop-up est toujours ouverte), la variable capturée dans la fenêtre parente, ainsi que tout le code afférant reste valide. Ainsi, le framework est toujours accessible via la variable Skynet alors même que la page qui a chargé ce fichier a disparu.
Mesures
Les temps sont inchangés au niveau de master1.html.
Au niveau de page1.html (les temps sont mesurés à partir du chargement de cette page) :
début de l’évaluation de la méthode init | 8ms |
Lancement de l’événement onload | 44ms |
Constat
Autrement dit, les gains sont très importants au niveau de la fenêtre pop-up qui n’a plus à charger le fichier skynetlib.js ce qu’on peut vérifier dans la vue réseau de firebug :
Dans l’étape précédente, la méthode init était appelée au bout de 1100ms, en partageant le framework, elle est appelée au bout de 8ms.
La méthode init de la fenêtre pop-up peut être exécutée très rapidement : la fenêtre pop-up peut utiliser pratiquement immédiatement le code du framework chargé dans la fenêtre parente master1.html.
Algorithme final
Principe
On peut maintenant récupérer le framework sans le charger de nouveau dans page1.html. Cependant, pour le moment, la page1.html ne peut plus être chargée directement. Il faut obligatoirement qu’elle ait été chargée via un window.open. et que la fenêtre qui a déclenché cette ouverture ait elle-même chargé le framework. Il faut corriger ce défaut.
On peut facilement détecter si une fenêtre a été ouverte via un window.open ou un document.location, il suffit de regarder si elle possède un window.opener et si window.opener != window (cf. le paragraphe sur le chargement dans la même fenêtre)
L’idée est donc de :
1) Détecter si la fenêtre a été ouverte directement
2) Si la fenêtre a été ouverte directement, ou a remplacé la page qui l’a ouverte, déclencher un chargement dynamique du framework
Mise en oeuvre
L’algorithme final reste assez simple :
- soit la fenêtre courante a été ouverte via une fenêtre parente et on récupère la variable Skynet via window.opener.Skynet
- soit on charge dynamiquement le fichier skynetlib.js qui initialise (localement) la variable Skynet.
Les exemples en ligne sont disponibles ici
Le fichier compressé comprenant toutes les pages de l’exemple sont ici
Conclusion
Début de init | Début de onload | |
Chargement synchrone par la page | 1221ms | 1280ms |
Chargement asynchrone par la page | 1266ms | 107ms |
Partage du framework | 8ms | 44ms |
Nos mesures montrent que lorsqu’on veut utiliser une fenêtre pop-up, il est très intéressant de partager le code du framework.
C’est d’autant plus important que généralement les fenêtres pop-up sont des fenêtres très utilisées : elles servent par exemple à éditer les objets manipulés par la fenêtre principale. Par exemple on peut avoir une page principale comportant un calendrier et une page pop-up qui permet d’éditer ces événements. Il est clair que le chargement de la page pop-up doit être extrêmement rapide. Dans notre exemple, nous avons vu qu’en partageant le framework, on peut utiliser son code au bout de quelques millisecondes (au lieu d’attendre que son code soit chargé puis évalué).
En plus, nous n’avons compté dans les mesures effectuées que le temps d’interprétation du fichier javascript. Nous n’avons pas tenu compte du temps nécessaire à la récupération des fichiers eux-mêmes. Le gain issu du partage du framework est donc encore supérieur. Sans ce partage, l’utilisateur est donc confronté à un délai d’attente plus important.