Interagir avec le DOM au cours du chargement

Problématique

En termes d’ergonomie, il est souhaitable qu’une page HTML se charge le plus vite possible ou en tout cas que les composants graphiques qu’elle comporte soient manipulables dès que possible par l’utilisateur. Lorsqu’on utilise un framework javascript (et qu’on n’utilise pas la méthode du partage de code cf. dernier article), il faut charger le code correspondant. Sur la plupart des navigateurs actuels, l’interprétation du code javascript est bloquante vis-à-vis de l’interprétation du DOM. Autrement dit, si la page est en train de se charger, qu’elle rencontre une balise SCRIPT, l’HTML cesse d’être interprété pendant que le contenu de la balise SCRIPT (le code javascript) est exécuté.

Pour cette raison, il est préférable d’interpréter le code javascript à la fin de la page ou après le onload (le DOM étant totalement chargé à ce moment). Les seules choses qu’on peut paralléliser sont le déclenchement du chargement de fichier javascript en parallèle. Néanmoins, une fois que le fichier est récupéré sur le poste client, son interprétation devient bloquante à son tour.

Ainsi, si on déclenche le chargement dynamique d’un fichier javascript dans le head ; si l’interprétation de ce fichier prend une seconde et a lieu avant la fin de l’affichage de la page HTML, on a une interruption de une seconde dans l’affichage.

Pour une question d’ergonomie, on en serait donc réduit à ne faire aucun traitement javascript lors du chargement de la page (le chargement de la librairie et les traitements associés étant déclenchés par le onload). Cela aurait des conséquences en termes … d’ergonomie. En effet, l’utilisateur risque de voir une première version (purement HTML) de la page, puis des corrections visuelles réalisées par le javascript.

Prenons l’exemple d’une page comportant des éléments de formulaire HTML. Cette page est entièrement statique et c’est sur le poste client, en javascript, que l’on souhaite initialiser les valeurs des éléments de formulaire (dans la page HTML, les attributs value sont vides).

La solution la plus simple si on veut charger la librairie javascript dynamiquement est :

  • Chargement de la page HTML (les value sont vides)
  • Chargement asynchrone de la librairie (déclenchée en bas de la page ou dans le onload)
  • Interprétation de la librairie (1 seconde)
  • Modification des value des composants (uniquement lorsque la librairie est chargée)

Cette solution se traduit par une page HTML qui se charge très vite, mais les champs de saisie restent vides pendant plus d’une seconde. C’est donc tout à fait perceptible par l’utilisateur.

Introduction

Nous abordons dans cet article une méthode qui permet de concilier les avantages d’un chargement dynamique des librairies javascript tout en garantissant une certaine ergonomie d’utilisation.

Lors du chargement de la page, de façon à ce que l’utilisateur ne puisse pas percevoir les changements dus à javascript, on va les faire au fil de l’eau, dès que chaque nœud HTML sur lesquels on veut faire un changement apparaît.

Comment un nœud DOM prévient quand il est prêt

On souhaite donc pouvoir déclencher un traitement javascript sur un nœud DOM au plus tôt, c’est-à-dire dès que le nœud existe.

On sait que :

  • les balises SCRIPT inline sont lues séquentiellement dans l’ordre du code source HTML.
  • un script javascript a immédiatement accès au DOM du document, même si celui-ci est encore en court de chargement.

Par ailleurs, on souhaite être conforme aux préconisations de l’unobtrusive javascript : on veut séparer les balises HTML du code javascript. Il n’y aura donc pas de handler d’événement javascript déclaré dans une balise HTML.

Une solution la simple (et portable) est donc d’insérer une balise SCRIPT à proximité immédiate de la balise DOM sur laquelle on souhaite réaliser un traitement.

Pour une balise auto-fermante comme INPUT, par exemple :

<INPUT value="Smith" />

çà donne:

<INPUT value="Smith" />
<SCRIPT>…</SCRIPT>

On est certain que

Pour une balise classique, telle que :

<DIV>
<balise>…</balise>
<balise>…</balise>
</DIV>

on peut insèrer la balise SCRIPT immédiatement à l’intérieur de la balise HTML :

<DIV>
<SCRIPT>…</SCRIPT>
<balise>…</balise>
<balise>…</balise>
</DIV>

Dans ce cas, le script a accès à la balise englobante (d’identifiant c1) mais pas aux autres balises.

On peut aussi insérer la balise SCRIPT à la fin de la balise HTML :

<DIV>
<balise>…</balise>
<balise>…</balise>
<SCRIPT>…</SCRIPT>
</DIV>

Dans ce cas, le script a accès à la totalité des balises filles de la balise englobante.

On peut aussi (ce qui a priori ne change pas grand-chose par rapport à la solution précédente), utiliser la même approche que pour les balises auto-englobantes, c’est-à-dire insérer le script après :

<DIV>
<balise>…</balise>
<balise>…</balise>
</DIV>
<SCRIPT>…</SCRIPT>

Mise en œuvre

Nous allons exclure de cet article les solutions consistant à interroger le body du document pour déterminer quelle est la balise courante en cours de lecture. Nous prendrons donc une solution plus simple. Cette solution impose la contrainte d’associer un attribut id unique à chaque nœud HTML concerné. Même si la balise SCRIPT est située à l’intérieur de la balise en cours de lecture, on peut déjà accéder à cette balise.

Ainsi, la forme suivante est parfaitement fonctionnelle :

<DIV>
<SCRIPT>
// Récupération du noeud DOM
var domEl=this.document.getElementById("c1");
</SCRIPT>
<DIV>…</DIV>
<DIV>…</DIV>
</DIV>

domEl contient bien le nœud DOM correspondant à la balise DIV d’identifiant c1, même si la balise SCRIPT est située à l’intérieur du DIV et que les balises suivantes n’ont pas encore été évaluées. Bien entendu, si on fait domEl.childNodes, on ne récupère que la balise SCRIPT, pas les balises d’identifiants a et b.

Cette solution utilisant les identifiants est parfaitement adaptée à une génération côté serveur. Par exemple, en JSP, on pourrait avoir une balise

<skynet:input id="c1" name="${path}">

qui générerait le fragment complet suivant :

<INPUT id="c1" name="p1.prenom" />
<SCRIPT>
// Récupération du noeud DOM
var domEl=this.document.getElementById("c1");
… // Actions à faire sur le DOM
</SCRIPT>

Exemple

Cet exemple est disponible en ligne ici.

Ajout d’un handler d’événement

On veut séparer le code de la balise HTML et la déclaration de son handler d’événement javascript.

Il s’agit ici d’un bouton de type submit :

<INPUT value="Valider">

Si javascript n’est pas activé, le bouton a le comportement normal d’un bouton submit (envoi du formulaire sur le serveur).

Si javascript est activé, on associe au bouton un handler d’événement ouvrant un dialogue demandant si on veut vraiment soumettre le formulaire.

Le code résultant est le suivant :

<INPUT value="Test">
<SCRIPT>
// Récupération du dom element (le bouton)
var domEl=this.document.getElementById("button1");
/** Le handler du bouton
*  Il empêche la validation du formulaire (returnValue=false)
* @param ev {Event}
**/
function verifierValidation(ev){
if (confirm("Voulez vous vraiment valider ?")) {
ev.returnValue=true;
} else {
ev.returnValue=false;
}
};
// Ajout du handler d’événement
if (domEl.addEventListener) {  // Firefox, Google Chrome, Safari, Opera
domEl.addEventListener ("click", verifierValidation, false);
}
else {
if (domEl.attachEvent) {   // IE
domEl.attachEvent ("onclick", verifierValidation);
}
}
</SCRIPT>

Il est facile d’utiliser ce mécanisme pour que les handlers d’événements ne soient plus déclarés en tant qu’attributs HTML (comme onclick par exemple) mais dans un code javascript séparé. En faisant cette déclaration immédiatement, on s’assure que certains comportements javascript seront déjà actifs avant le chargement de la librairie du framework. Dans notre exemple, l’utilisateur ne pourra pas cliquer sur le bouton « Valider » sans passer par la vérification javascript, même si la librairie n’a pas encore été chargée.

Ajouter un fragment HTML au DOM si javascript est activé

On veut générer un fragment HTML à attacher au nœud DOM existant.

On applique la première solution à la génération d’un icône de calendrier permettant de lancer une pop-up de saisie des dates.

Si javascript est désactivé, seul le champ de saisie textuel est présent.

<INPUT type="text" value="">
<SCRIPT>
var domEl=this.document.getElementById("calendar1");
domEl.insertAdjacentHTML("afterEnd",
"<IMG src='./Agenda-Icon.gif' onclick='launchCalendar()' />"); </SCRIPT>

Noter qu’il faut bien évidemment que la fonction launchCalendar soit définie à ce moment. Parce que sinon, si l’utilisateur clique immédiatement dessus, il y aura une erreur.

Cela implique qu’il faudra identifier et différencier :

  • les traitements à réaliser AVANT le chargement de la librairie (leur code doit être défini avant leur utilisation, on peut par exemple les définir dans le head du document),
  • les traitements standards (qui nécessitent le chargement de la librairie complète)

Autres utilisations

On peut multiplier à loisir les utilisations de cette approche.

Dans le cadre de l’unobtrusive javascript, on a vu l’ajout des events listeners ou l’insertion conditionnelle de fragments html, on pourrait aussi rendre visible ou changer l’état d’éléments HTML générés sur le serveur.

Cette approche permet aussi de profiler les temps d’interprétation du DOM : il suffit que chaque balise SCRIPT insérée fasse des mesures du temps.

De façon générale, cette approche permet de réaliser des modifications du code HTML de la page sans pour autant que l’utilisateur puisse les percevoir. Çà permet par exemple d’utiliser une page HTML complètement statique et de modifier au chargement les attributs value et l’état (disabled, readonly) de chaque composant. Cette solution ne fonctionne évidemment plus sans javascript (les composants graphiques n’ont pas la bonne value) mais elle peut être utilisée pour réutiliser la même page avec des valeurs différentes sans appel serveur (devient très intéressante en terme de performance : le serveur d’application se contentant de générer valeurs et états et pas la page HTML qui peut être gérée par un serveur web.

Constat

Cette approche permet de réaliser des modifications javascript sur la page en cours de chargement sans pour autant qu’on soit obligé d’avoir chargé avant la librairie javascript gérant les composants graphiques. Cette approche permet en effet d’afficher très rapidement le contenu de la page HTML (sans être retardé par des chargements de code javascript, ceux-ci peuvent par exemple avoir lieu au onload de la page). Cela permet de garantir à l’utilisateur qu’il a accès immédiatement (en cours de chargement) à certaines fonctionnalités ou tout du moins que les modifications réalisées par javascript ne sont pas visuellement perceptibles.

Bien sûr, l’interprétation de l’HTML de la page est légèrement plus lente (mais çà reste à peine perceptible tant que les traitements javascript réalisés sont simples – et que ceux-ci ne nécessitent pas à une évaluation longue de code javascript –).

Les composants semblent être correctement initialisés.

Ce contenu a été publié dans framework, Javascript, optimisation, avec comme mot(s)-clé(s) , , , , . Vous pouvez le mettre en favoris avec ce permalien.