Une migration vers ES6 et un pas de plus en écoconception

le 6 décembre 2016, par Bruno Thomas

Actuellement je travaille sur un site web en mode SaaS pour travailleurs indépendants et professions libérales. Comme le javascript n’était pas ma tasse de thé, j’avais négligé cet aspect, en accumulant le minimum de code (backbone/semantic-ui) dans de petits fichiers qui commençaient à grossir. Conscient de cette dette technique qui s’y accumulait, il y a une dixaine de jours je suis allé aux Javascript Les 10 Doigts Dedans (ou JSLDD pour les intimes) afin de rencontrer des férus du JS qui pourraient me faire aimer un peu mieux le JS et prendre soin de cet aspect incontournable du site.

L’objectif est double :

  • diminuer la dette technique : rationnaliser l’utilisation du JS dans le HTML, améliorer la testabilité du code JS, la lisibilité du code de production, s’ouvrir des portes d’amélioration du design du code (par exemple pouvoir intégrer plus simplement des librairies comme riot.js)
  • diminuer la taille des pages, en minifiant les fichiers javascript du site (pour les librairies, j’utilisais déjà les versions minifiées)

Et enfin, pouvoir apprécier cet écosystème du frontend ?

amélioration du javascript

J’ai binômé avec Paul qui m’a montré comment faire pour mettre en oeuvre une stack minimale ES6. En tant qu’expert frontend il m’a conseillé de mettre en place un build JS pour le code du front. Il m’a montré le gestionnaire de packets JS yarn, babel pour la transpilation en ES5, mocha pour le test runner, expect pour les assertions. Nous avons pu transformer un bout de code que j’avais amené et des tests unitaires relatifs.

Emballé par le résultat (vous pouvez voir l’historique des changements),

export class Node {
  constructor(name, data, children = undefined) {
    this.name = name;
    this.data = data;
    this.children = children;
  }

  is_dir() {
    return this.children !== undefined;
  };
}

export class FileSystemTree {
  constructor(values) {
    this.root = build_tree(values);
  }
}

function build_tree(values) {
  let root = new Node('', {id: 'root', filename: ''}, []);
  values.forEach(value => {
    let path_items = value.filename.replace(/^\//, '').split('/');
    let node = new Node(path_items[path_items.length - 1], value);
    mkdir_p(path_items, root).children.push(node);
  });
  return root;
}

function mkdir_p(path_items, node) {
  let path_item = path_items[0];
  let n = node.children.find(_node => {return _node.name == path_item ? _node: undefined});
  if (n === undefined) {
    path_items.pop();
    let current_node = node;
    path_items.forEach(dir => {
      let new_dir = new Node(dir, {filename: current_node.data.filename + '/' + dir}, []);
      current_node.children.push(new_dir);
      current_node = new_dir;
    });
    return current_node;
  } else {
    path_items.shift();
    return mkdir_p(path_items, n);
  }
}

j’ai modifié la hiérarchie de mon source pour séparer le code backend et code frontend. Puis j’ai repris tous les bouts de code javascript du site et les ai transformés pour qu’ils puissent passer à la moulinette babel. Même si je n’ai pas mis en oeuvre immédiatement toutes les possibilités de syntaxe offertes par ES6 (classes, etc), les impacts étaient assez importants.

Ce que j’ai appris :

  • c’est un refactoring d’architecture, et je n’aurais pas pu éviter des régressions sans les tests unitaires JS et les tests d’acceptance sélénium
  • l’ES6 amène une bien meilleure clareté même si les changements ne sont pas si drastiques que ça, en évitant les niveaux ({ ... }) et les définitions MyClass.prototype.blah = function() {...}. L’encapsulation avec les classes, des boucles plus claires avec la flèche =>, des variables au scope limité avec let my_var. Une gestion des modules qui ressemblerait presque à du python, et plein d’autres choses encore
  • webpack rocks ; j’ai ajouté webpack pour pouvoir packager le code et le déployer dans le site. Cela permet notamment de regrouper ses fichiers javascript et de diminuer le nombre de requêtes HTTP
  • j’ai un build js et un build python mais ça fonctionne bien : les tests unitaires sont lancés en ligne de commande (avant c’était avec jasmine dans un navigateur). La sortie de webpack (les fichiers JS minifiés et regroupés par page) est copiée dans la partie statique du module web python

des améliorations pour l’utilisateur et pour l’empreinte écologique du site

J’ai voulu alors constater la diminution de la taille des pages. Je me suis que je pouvais exécuter les tests d’acceptance avant et après refactoring, en mesurant la taille des pages avec tcpdump :

sudo tcpdump -v  -i eth0 port 443 | grep -e 'length [0-9]*)' |  
  sed 's/.*length \([0-9]*\))/\1/' |  awk '{sum+=$1;print sum,$1}'

Je lance les tests, et observe les résultats :

Avant refactoring :

30156494 203
17126 packets captured

Après babel/webpack :

33176994 52
16874 packets captured

WOT ?!!? On observe moins de packets mais une taille plus importante de 10% environ.

Je vais voir ce qui se passe avec un navigateur. Je supprime le cache, charge la page de login et regarde ce que me dit firefox :

  • 11 requêtes, 1.4 Mo de transféré en 333ms avec :
  • login.html : 2.6 Ko
  • vendor.js : 733 Ko
  • login.js : 664 octets

Avec l’ancienne version :

  • 14 requêtes, 1 Mo de transféré en 268ms avec :
  • login.html : 3.1 Ko
  • semantic.min.js : 269 Ko
  • jquery.min.js : 82.6 Ko

Dans vendor.js, il y a semantic.js, jQuery.js, moment.js. Avant, j’avais des imports javascript un peu partout dans mon HTML (sauf jquery/semantic qui étaient dans le layout), c’était plus difficile à maintenir, mais c’était optimisé car je ne chargeais par exemple pickadate.js juste à l’endroit où j’avais des sélecteurs de date.

Une fois chargé, vendor.js est caché mais lorsque j’exécute les tests d’acceptance, à chaque feature, je relance un navigateur frais (pour l’isolation des tests). Chaque test ne passe pas forcément sur toutes les pages. Dans ces cas, moment/pickadate sont chargés systématiquement, ce qui n’était pas le cas avant.

Je continue mon auscultation de différentes pages du site, et relève les tailles et temps de chargement :

tests manuels

Si on fait la somme des tailles, il y a un léger avantage pour la version sans webpack/babel, mais si on exclut la première requête, le refactoring reprend l’avantage.

Pour simuler une utilisation plus longue, un injecteur de tests de charge pourrait aider. J’essaye funkload, sans succès car il charge les CSS et les images, mais pas le javascript. Par ailleurs, funkload trace le traffic réseau global mais pas la taille par page.

Je me reporte sur JMeter que j’avais déjà utilisé il y a déjà quelque temps. Il faut dépasser l’IHM java/swing assez moche, se replonger dans les termes un peu baroques (Groupes d’unité, Fragment d’élément, récepteur d’assertions…) pour construire un plan de test.

Config jmeter

Ne pas oublier de cliquer sur “récupérer les ressources incluses” dans les requêtes HTTP, puis d’ajouter un gestionnaire de cache HTTP pour qu’il se comporte comme un navigateur avec les dépendances Javascript/CSS/Images. Mais une fois configuré il fait bien son boulot, et me permet de faire 100 itérations avec un seul utilisateur sur un scénario qui charge toutes les pages. Ce scénario pourra toujours me resservir pour faire quelques tests de charge sur le serveur, je n’aurai qu’à modifier le paramètre du nombre d’utilisateurs concurrents.

A présent les résultats sont bien différents :

Diagramme synthèse

On passe d’une taille de page moyenne de 12,14 Ko à 9,78 Ko, c’est à dire qu’on gagne 20%, avec un code JS plus clair et mieux intégré au HTML. Nous gagnons également 7% de temps de chargement (sur un lien local entre 2 containers docker). Le jeu en valait la chandelle. En revanche, on peut voir que les résultats en terme de gains de bande passante d’un tel refactoring dépendent beaucoup de l’usage qui est fait du site par les utilisateurs. L’avantage, lorsque le pas a été franchi, c’est qu’on peut modifier assez facilement la structuration des fichiers source Javascript avec Webpack, pour adapter l’architecture des pages en fonction des usages.

L’étape suivante sera l’utilisation de fichiers javasript avec un hash code, pour recharger uniquement les fichiers modifiés. Actuellement nous utilisons des chemins virtuels avec la version du site. Cela oblige les navigateurs de chaque utilisateur à recharger toutes les resources (js/css/images) lorsque le site a évolué. Ou encore la gestion des ressources CSS avec webpack.

PS : merci à ut7 pour l’organisation du JSLDD et Paul pour son binômage instructif

Utiliser docker-compose avec le réseau docker

le 1 octobre 2016, par Bruno Thomas

Depuis la version 1.6 de docker-compose et la version 1.10 de docker, une gestion nouvelle du réseau dans les conteneurs docker a été implémentée. Quand vous avez des dépendances entre plusieurs conteneurs, plus besoin de faire des liens (links) entre eux. Pour les environnements de dévelopement c’est très pratique, notamment pour le problème des liens bidirectionnels.

Installer une chaîne SSL avec dovecot

le 22 juillet 2016, par Bruno Thomas

Nous avons entamé avec quelques amis développeurs notre cure de dégooglelisation il y a quelques années. Pour le mail, nous avons choisi, aussi pour notre apprentissage, de monter notre serveur postfix/dovecot. Pour ceux que ça intéresse, mais qui ne veulent pas mettre les mains dans le cambouis, il y a des infos chez Framasoft.

Une équipe agile, c'est comme l'équipe du Dr House

le 18 avril 2016, par Jean-Philippe Caruana

L’autre soir, je regardais un épisode de Dr House (oui, ça arrive) et j’ai été frappé par la similitude entre le fonctionnement de son équipe et celui d’une équipe agile.

Le A3 comme outil de communication avec le management

le 12 avril 2016, par Bruno Thomas

Il y a peu encore, je travaillais pour un groupe de télécom qui commercialise une messagerie instantanée d’entreprise permettant aux internautes de communiquer avec le service client depuis un site web. Il comprend une distribution des chats dans des files d’attentes, des outils d’aide pour les conseillers et des outils de supervision/administration.

Astuce - lancer un conteneur docker comme un service système

le 8 octobre 2015, par Jean-Philippe Caruana

Je ne vais pas ici vous faire une introduction à docker, ce n’est pas l’objet de ce post.

MongoDb le cluster qui bagote, la suite

le 23 septembre 2015, par Bruno Thomas

Suite de notre épisode précédent sur les bagots mongo, nous reprenons où nous nous étions arrêtés : nous avons un test d’acceptance instable et nous avons acquis la certitude que le cluster mongo en est la cause. Soit il est mal configuré soit buggué.

Être alerté en cas de OutOfMemoryException

le 7 septembre 2015, par Jean-Philippe Caruana

Un des plus grands dangers quand on exploite un service en production, c’est de ne pas être au courant qu’un problème a eu lieu. Un des problèmes graves que l’on peut rencontrer est l’arrêt d’une JVM qui n’a plus assez de mémoire disponible. D’innombrables causes peuvent en être l’origine, mais ce n’est pas le sujet de ce billet. Ici, nous allons voir comment en être alerté, afin de pouvoir réagir (relancer le service par exemple). Il faudra ensuite que l’équipe se penche sur le problème pour éviter que cela ne se reproduise.

Optimiser son client ssh

le 25 août 2015, par Jean-Philippe Caruana

Dans mon travail quotidien, je me connecte très souvent à une multitude de serveurs, que ce soit pour aller sur le serveur de logs, pour faire une mise en production ou faire de la maintenance système. Je me connecte également très souvent (sans le savoir) aux serveurs de github à chacune de mes actions git pull et git push.