Éco-conception Web : HTML ou JSON ?

HTML 10 - JSON 1

Avec un exemple simple, nous montrons que manipuler du HTML à la place du JSON est bien plus performant et donc plus raisonnable sur le plan de la consommation énergétique.

La tendance actuelle est d’utiliser du code JavaScript sur le terminal du client pour produire l’interface utilisateur à partir de données au format JSON. Les navigateurs Web ne sachant afficher que du HTML (mis en forme via des CSS), le code JavaScript doit produire des éléments HTML et c’est là qu’est le problème !

JavaScript est un langage interprété, il est plus ou moins rapide d’un navigateur à un autre et de belles optimisations ont été réalisées pour qu’il soit le plus efficace possible. Mais jamais il n’égalera du code machine ! Hors les navigateurs sont développés avec des langages compilés comme C++, donc la conversion d’un texte HTML en éléments pour le DOM est d’une vitesse inégalable.

Démonstration

Pour s’en convaincre, voici un petit exemple où sont affichés des résumés d’articles d’actualité (au moins une centaine d’un coup pour se rendre compte de la différence) avec une source au format JSON et une source au format HTML.

Le test mesure uniquement le temps de conversion de la réponse en éléments du DOM, donc ni le temps de chargement, ni le temps d’insertion des éléments dans la page.

Cliquez sur le bouton suivant pour lancer le test : 

Les résumés d’articles présentés ci-dessous proviennent des fils RSS du site franceinfo.

Version JSON : 
Version HTML : 

Rapport des temps JSON / HTML : 
Moyenne du rapport : 

En recommençant plusieurs fois le test, vous pouvez constater que la version HTML est toujours plus rapide que la version JSON quel que soit le navigateur.

Par exemple sur un MacBook Pro 2020, avec une centaine d’essais, la version HTML est en moyenne :

  • 7 fois plus rapide avec un navigateur basé sur le projet Chromium (Chrome, Edge, etc.),
  • 15 fois plus rapide avec Firefox,
  • 28 fois plus rapide avec Safari.

Le temps de traitement de la version HTML est grosso modo le même entre tous les navigateurs sur un même terminal, les différences des moyennes viennent donc de la performance de l’interpréteur JavaScript, celui de Chromium est le plus rapide, Firefox est moins bon et celui de Safari encore moins.

Code de la version HTML

Pour la version HTML, le code JavaScript qui réalise la requête et le traitement du résultat ressemble à celui-ci :

fetch(url).then(r => r.text()).then(infos => {
let startTime = performance.now();
let articles = new DOMParser().parseFromString(infos, "text/html").body;
let endTime = performance.now();
timeContainer.textContent = `${endTime - startTime} ms`;
articlesContainer.append(...articles.children);
}).catch(() => articlesContainer.textContent = "Impossible de récupérer les données.");

Les données récupérées sont une suite d’éléments <article> sous forme de texte, la fonction parseFromString() d’une instance de l’objet DOMParser est utilisée pour convertir ce texte en une série de HTMLElement injectables dans le DOM. C’est uniquement l’appel de cette fonction qui est mesuré.

Code de la version JSON

Pour la version JSON, le code JavaScript qui réalise la requête et le traitement du résultat ressemble à celui-ci :

fetch(url).then(r => r.json()).then(infos => {
let startTime = performance.now();
let articles = infos.map(infoItem);
let endTime = performance.now();
timeContainer.textContent = `${endTime - startTime} ms`;
articlesContainer.append(...articles);
}).catch(() => articlesContainer.textContent = "Impossible de récupérer les données.");

Les différences essentielles par rapport à la version HTML sont l’utilisation de la fonction json() à la place de la fonction text() pour traiter la réponse de la requête et l’utilisation de la fonction map() sur le tableau d’objets JavaScript récupérés en lui donnant une fonction infoItem(). C’est l’appel de la fonction map() qui est mesuré.

La fonction infoItem() s’occupe de produire un élément <article> et son contenu pour un objet du tableau JSON reçu.

Le code JavaScript de cette fonction ressemble à celui-ci :

function infoItem(item) {
return h("article", null,
h("h4", null, item.title),
h("time", {datetime: item.pubDate}, infoDateTime(item.pubDate)),
h("div", null,
item.image ? h("img", {src: item.image, alt: item.title, loading: "lazy"}) : null,
h("p", null,
item.description,
h("br"),
item.link ? h("a", {href: item.link, target: "_blank", rel: "noopener"}, "Lire la suite") : null
)
)
);
}

La fonction infoItem() utilise une fonction utilitaire h() qui retourne un objet Element avec les attributs et le contenu passés en paramètre via des appels aux fonctions createElement(), setAttribute() et append().

Le code JavaScript de la fonction h() ressemble à celui-ci :

function h(tagName, attributes, ...children) {
let element = document.createElement(tagName);
attributes && Object.entries(attributes).forEach(([k, v]) => element.setAttribute(k, v));
children && element.append(...children.filter(e => e));
return element;
}

Remarques

Il est logique que la version HTML soit plus rapide que la version JSON, la version HTML traite en une passe la totalité des données via la fonction parseFromString() hors cette fonction est implémentée dans le code natif navigateur. La version JSON fait des appels en grand nombre aux fonctions createElement(), setAttribute() et append() qui sont aussi implémentées dans le code du navigateur mais qui provoquent forcément des allers-retours entre l’interpréteur JavaScript et le code natif du navigateur.

La fonction parseFromString() est vraiment très efficace, elle traite allègrement 130 ko de HTML brut en 2 ou 3 millisecondes. Avec un terminal équipé d’un microprocesseur très puissant, il est même parfois impossible de mesurer le temps de traitement, la méthode performance.now() n’étant pas assez précise.

Du côté des données échangées, JSON est logiquement plus performant au niveau de la taille, dans les 130 ko en HTML contre 97 ko en JSON soit un rapport de 1,34. Mais avec la compression gzip, la taille réelle qui transite est autour de 38 ko en HTML contre 36 ko en JSON, soit un rapport de 1,06.

Du côté serveur, les temps pour produire le HTML ou le JSON sont équivalents (une fois que les flux RSS ont été traités).

Vous pouvez retrouver ces informations en ouvrant l’inspecteur Web sur cette page dans l’onglet Réseau, en filtrant sur les requêtes Fetch/XHR et en cochant Désactiver le cache (les requêtes sont mise en cache pendant 10 minutes) puis en cliquant sur le bouton Tester de la page.

Conclusion

Doit-on systématiquement utiliser du HTML à la place du JSON ? Non et pour plusieurs raisons :

  • Si la source du HTML n’est pas sure (externe), il y a le risque d’une attaque XSS, une nouvelle version d’une Sanitizer API est en préparation mais encore loin d’être disponible. Il n’y a par contre pas de problème si vous êtes le maître de la source.
  • Si votre serveur alimente un site / application Web et une application non Web (mobile native par exemple), JSON est alors la meilleure solution car compatible avec les deux types d’applications.
  • Si des calculs sont à réaliser avec les données, ou si elles ne sont pas visualisées directement en HTML, par exemple des données servant à construire un graphique ou devant être visualisées sur une carte, etc.
  • Si les données sont modifiables par l’utilisateur, les recevoir en HTML suppose d’implémenter du code pour les extraire afin de pouvoir ensuite envoyer les modifications. Ceci dit, les formulaires HTML fonctionnent très bien pour ça !

En conclusion, utiliser du HTML à la place du JSON est préférable quand cela est possible. Cela n’a pas d’impact, en coût de développement (le code qui n’est pas à faire en JavaScript côté client est à faire côté serveur pour produire le HTML), ni en performance côté serveur, ni en coût de transfert de données, mais engendre par contre une réduction de consommation d’énergie pour le client. Certes, être au moins dizaine de fois plus rapide quand on est à l’échelle d’une centaine de microsecondes semble désuet, mais en multipliant par le nombre d’utilisateurs, cela peut vite chiffrer.