Javascript Responsable : 2ème Partie

Publié le , par Fredéric Pineau
Responsable

No Fucking JS Spirit

Préambule

Cet article fait suite à l’article JavaScript Responsable : 1ère PartiePour rappel, le JavaScript Responsable est la formulation de Jeremy Wagner, moins provocatrice que notre No Fucking JS Spirit mais dont la finalité reste la même : remettre en cause la façon dont nous utilisons JavaScript.

Au programme, dans ce magnifique texte (avec un certains nombre de barbarismes informatiques) : la quête du plus petit code, un service différent des fichiers JavaScript et contrairement à ce qu’on pourrait penser moins de transpilation !

Bonne lecture.

Introduction

Vous et le reste de l’équipe de développement avez fait pression avec enthousiasme pour une refonte totale du site web vieillissant de l’entreprise. Vos demandes ont été entendues par vos responsables – et même jusqu’à la direction – qui a donné le feu vert. Heureux, vous et l’équipe avez commencé à travailler avec les équipes de design, de reprographie et d’analyse d’impact. En peu de temps, vous avez écrit le nouveau code.

Tout a commencé innocemment avec un npm install ici et un npm install là. Avant même de vous en rendre compte, vous installiez des dépendances de production comme un étudiant de premier cycle qui fait des concours d’alcool sans se soucier du lendemain matin.

Puis vous avez lancé le site.

Contrairement aux conséquences de la plus grosse des beuveries, la douleur n’a pas commencé le lendemain matin. Oh, non. Ce n’est que des mois plus tard que les products owners et les managers ont éprouvé des nausées et des maux de tête épouvantables en se demandant pourquoi les conversions et les revenus avaient diminué depuis le lancement. Ils ont ensuite fait une forte fièvre lorsque le directeur technique est revenu d’un week-end à la campagne et demandé pourquoi le site se chargeait si lentement sur son téléphone – si au moins il s’est chargé.

Tout le monde était heureux. Maintenant personne n’est heureux. Bienvenue dans votre première gueule de bois JavaScript.

Ce n’est pas de votre faute

Lorsque vous êtes aux prises avec une vicieuse gueule de bois, “Je vous l’avais bien dit” serait une attitude amplement méritée et provocante – en supposant que vous pourriez quand même combattre dans un état si désolant.

Lorsqu’il s’agit d’une gueule de bois JavaScript, il y a beaucoup de blâmes à distribuer. Mais pointer du doigt est une perte de temps. Le paysage du web d’aujourd’hui exige que nous itérions plus vite que nos concurrents. Ce genre de pression signifie que nous sommes susceptibles de profiter de tous les moyens disponibles pour être aussi productifs que possible. Cela signifie que nous sommes du coup plus susceptibles – mais pas nécessairement condamnés – de créer des applications avec plus de surcharges et d’utiliser des modèles qui peuvent nuire aux performances et à l’accessibilité.

Le développement web n’est pas facile. C’est un gros travail que l’on réussit rarement du premier coup. Cependant, la meilleure partie du travail sur le web est que nous n’avons pas besoin d’obtenir la perfection du premier coup. Nous pouvons apporter des améliorations après coup, et c’est à cela que sert le deuxième volet de cette série d’articles. La perfection est loin d’être atteinte. Pour l’instant, prenons le dessus sur cette gueule de bois JavaScript en améliorant de votre site à court terme.

Rassemblez les suspects habituels

Ça peut sembler évident, mais cela vaut la peine de passer en revue la liste des optimisations de base. Il n’est pas rare que de grandes équipes de développement, en particulier celles qui travaillent dans de nombreux dépôts ou qui n’utilisent pas de boilerplate optimisé, les négligent.

Faites du tree shaking

Tout d’abord, assurez-vous que votre chaîne de compilation est configurée pour faire du tree shaking. Si vous ne connaissez pas encore le tree shaking, j’ai écrit un guide à ce sujet l’an dernier que vous pouvez consulter. En résumé, le tree shaking est un processus dans lequel les exportations inutilisées dans votre code source ne sont pas compris dans vos bundles de production.

Le tree shaking est disponible avec des bundlers modernes comme  webpackRollup, ou ParcelGrunt ou gulp – qui ne sont pas des bundlers, mais plutôt des programmes d’exécution des tâches – ne le feront pas pour vous. Un programme d’exécution de tâches ne construit pas un graphique de dépendances comme le fait un bundler. Ils exécutent plutôt des tâches discrètes sur les fichiers que vous leur fournissez avec un nombre quelconque de plugins. Les exécuteurs de tâches peuvent être étendus avec des plugins afin d’utiliser les bundlers pour traiter JavaScript. Si étendre les exécuteurs de tâches de cette façon est problématique pour vous, vous aurez probablement besoin d’effectuer un audit manuel et de supprimer le code inutilisé.

Pour que le tree shaking soit efficace, les éléments suivants doivent être en mis en place :

  1. Votre logique d’application et les packages que vous installez dans votre projet doivent être créés sous forme de modules ES6. Faire du tree shaking sur les modules CommonJS n’est pas possible.
  2. Votre bundler ne doit pas transformer les modules ES6 en un autre format de module au moment de la construction. Si cela se produit dans une chaîne de compilation  qui utilise Babel, la configuration  @babel/preset-env doit spécifier modules: false pour empêcher la conversion du code ES6 en CommonJS.

Au cas où il n’y aurait pas eu de tree shaking pendant la construction, il peut être utile de le faire. Bien sûr, son efficacité varie selon les cas. Cela dépend également du fait que les modules que vous importez introduisent ou non des effets secondaires, qui peuvent influencer la capacité d’un bundler à faire du tree shaking sur les exportations non utilisées.

Fractionner ce code

Il y a de bonnes chances pour que vous utilisiez une forme de fractionnement de code, mais cela vaut la peine de réévaluer la façon dont vous le faites. Peu importe comment vous fractionnez le code, il y a deux questions qui valent toujours la peine d’être posées :

  1. Dédoublez-vous le code commun entre les points d’entrée ?
  2. Utilisez-vous le lazy loading de toutes les fonctionnalités pour lesquelles c’est possible avec dynamic import() ?

Celles-ci sont importantes parce que la réduction du code redondant est essentielle à la performance. La fonctionnalité de lazy loading améliore également les performances en réduisant l’empreinte JavaScript initiale sur une page donnée. L’utilisation d’un outil d’analyse tel que Bundle Buddy  peut vous aider à déterminer si vous avez un problème de redondance.

Bundle Buddy peut examiner les statistiques de compilation de votre webpack et déterminer combien de code est partagé entre vos bundles.

 

En ce qui concerne le lazy loading, il peut être un peu difficile de savoir par où commencer à chercher des opportunités. Lorsque je cherche des opportunités dans des projets existants, je recherche des points d’interaction utilisateur dans l’ensemble du code source, tels que des activités de clic et de clavier, ainsi que des candidats similaires. Tout code qui nécessite une interaction de l’utilisateur pour s’exécuter est un bon candidat potentiel pour un import dynamique.

Bien entendu, le chargement de scripts à la demande peut entraîner un retard sensible de l’interactivité, car le script nécessaire à l’interaction doit d’abord être téléchargé. Si l’utilisation des données n’est pas un problème, pensez à utiliser l’indice de ressource rel=prefetch pour charger de tels scripts à une basse priorité qui ne se disputera pas la bande passante avec les ressources critiques. rel=prefetch est plutôt bien supporté, mais rien ne se brisera s’il n’est pas supporté, car les navigateurs ignorent les balises qu’ils ne comprennent pas.

Externaliser le code hébergé d’un tiers

Idéalement, vous devriez héberger vous-même autant de dépendances de votre site que possible. Si, pour une raison quelconque, vous devez charger des dépendances d’un tiers, marquez-les comme externes dans la configuration de votre bundler. Dans le cas contraire, les visiteurs de votre site Web pourraient télécharger à la fois le code hébergé localement et le même code à partir d’un tiers.

Regardons une situation hypothétique où cela pourrait vous nuire : disons que votre site charge Lodash à partir d’un CDN public. Vous avez également installé Lodash dans votre projet de développement local. Cependant, si vous ne marquez pas Lodash comme externe, votre code de production finira par charger une copie d’une tierce partie en plus de la copie groupée, hébergée localement.

Cela peut sembler du bon sens si vous connaissez bien les bundlers, mais j’ai observé qu’on ne s’en rend pas toujours compte. Ça vaut le coup de vérifier deux fois.

Si vous n’êtes pas convaincu d’héberger vous-même vos dépendances tierces, envisagez d’ajouter dns-prefetch, preconnect, ou peut-être même preload des indications pour elles. Cela peut réduire le temps d’interactivité de votre site et, si JavaScript est essentiel au rendu du contenu, le Speed Index de votre site.

Des alternatives plus petites pour moins de surcharge

Userland JavaScript est comme un immense magasin de bonbons, et nous, en tant que développeurs, sommes impressionnés par la quantité d’offres open source. Les frameworks et les librairies nous permettent d’étendre nos applications pour faire rapidement toutes sortes de choses qui autrement prendraient beaucoup de temps et d’efforts.

Bien que je préfère personnellement minimiser de façon radicale l’utilisation des frameworks et librairies côté client dans mes projets, leur apport est indéniable. Pourtant, nous avons la responsabilité d’être un peu va-t-en guerre quand il s’agit de ce que nous installons. Lorsque nous avons déjà construit et expédié quelque chose qui dépend d’une multitude de code installé à exécuter, nous avons accepté un coût de base que seuls les responsables de ce code peuvent pratiquement couvrir.

Peut-être, mais encore une fois, peut-être pas. Cela dépend des dépendances utilisées. Par exemple, React est extrêmement populaire, mais Preact est une alternative ultra-petite qui partage largement la même API et conserve la compatibilité avec de nombreux add-ons React. Luxon et date-fns sont des alternatives beaucoup plus compactes à moment.js, qui n’est pas vraiment minuscule.

Les librairies comme Lodash offrent de nombreuses méthodes utiles. Pourtant, certains d’entre eux sont facilement remplaçables par des ES6 natifs. La méthode compact de Lodash, par exemple, peut être remplacée par la méthode filter des array. Beaucoup d’autres peuvent être remplacées sans trop d’efforts et sans qu’il soit nécessaire de faire appel à une grande librairie d’utilitaires.

Quels que soient vos outils préférés, l’idée est la même : faites des recherches pour voir s’il existe des alternatives plus petites, ou si les fonctionnalités natives du langage peuvent faire l’affaire. Vous serez peut-être surpris du peu d’efforts qu’il vous faudra pour radicalement réduire les surcharges de votre application.

Desservez vos scripts différemment

Il y a de fortes chances que vous utilisiez Babel dans votre chaîne de compilation pour transformer votre source ES6 en code pouvant fonctionner sur les anciens navigateurs. Cela signifie-t-il que nous sommes condamnés à servir des bundles géants aux navigateurs qui n’en ont pas besoin, jusqu’à ce que les anciens navigateurs disparaissent complètement ? Bien sûr que non ! Le service différentiel nous aide à contourner ce problème en générant deux versions différentes de votre source ES6 :

  • Bundle one, qui contient toutes les transformations et polyfills nécessaires pour que votre site fonctionne sur les anciens navigateurs. Vous êtes probablement déjà en train de desservir ce paquet en ce moment.
  • Bundle deux, qui contient peu ou pas de transformations et de polyfills parce qu’elle cible les navigateurs modernes. C’est le bundle dont vous ne servez probablement pas, du moins pas encore.

La réalisation de cet objectif est un peu compliquée. J’ai écrit un guide sur la façon de le faire, il n’y a donc pas besoin d’approfondir dans cet article. En résumé, vous pouvez modifier votre configuration de build pour générer une version supplémentaire mais plus petite du code JavaScript de votre site, et l’envoyer uniquement aux navigateurs modernes. La bonne nouvelle est qu’il s’agit d’économies que vous pouvez réaliser sans sacrifier aucune des caractéristiques ou des fonctionnalités que vous offrez déjà. Selon le code de votre application, les économies peuvent être très importantes.

Analyse d’un webpack-bundle-analyzer d’un ancien projet (à gauche) par rapport à un bundle moderne (à droite).

Le modèle le plus simple pour servir ces bundles à leurs plateformes respectives est court. Il fonctionne également très bien dans les navigateurs modernes :


<!-- Modern browsers load this file: -->
<script type="module" src="/js/app.mjs"></script>
<!-- Legacy browsers load this file: -->
<script defer nomodule src="/js/app.js"></script>

Malheureusement, une mise en garde s’impose : les anciens navigateurs comme IE 11 – et même les navigateurs relativement modernes comme les versions 15 à 18 d’Edge – téléchargeront les deux bundles. Si c’est un compromis acceptable pour vous, ne vous inquiétez pas davantage.

D’un autre côté, vous aurez besoin d’une solution de contournement si vous vous inquiétez des conséquences sur les performances des anciens navigateurs qui téléchargeront les deux ensembles de bundles. Voici une solution potentielle qui utilise l’injection de script (au lieu des balises de script ci-dessus) pour éviter le double téléchargement sur les navigateurs concernés :


var scriptEl = document.createElement("script");

if ("noModule" in scriptEl) {
  // Set up modern script
  scriptEl.src = "/js/app.mjs";
  scriptEl.type = "module";
} else {
  // Set up legacy script
  scriptEl.src = "/js/app.js";
  scriptEl.defer = true; // type="module" defers by default, so set it here.
}

// Inject!
document.body.appendChild(scriptEl);

Ce script déduit que si un navigateur supporte l’attribut nomodule dans l’élément script, il comprend type=”module”. Cela garantit que les anciens navigateurs n’obtiennent que des scripts anciens et que les navigateurs actuels n’obtiennent que des scripts actuels. Faites attention, cependant, à ce que les scripts injectés dynamiquement chargent par défaut de manière asynchrone, donc définissez l’attribut async sur false si l’ordre des dépendances est crucial.

“Transpile” moins

Je ne suis pas là pour dénigrer Babel. C’est indispensable, mais bon sang, ça ajoute beaucoup de choses sans que tu le saches. Ça vaut la peine de jeter un coup d’œil sous le capot pour voir ce qui s’y trouve. Quelques changements mineurs dans vos habitudes de codage pourront avoir un impact positif sur ce que Babel déverse.

Bon à savoir : les paramètres par défaut sont une fonction ES6 très pratique que vous utilisez probablement déjà :


function logger(message, level = "log") {
  console[level](message);
}

Il faut faire attention au paramètre level, qui a la valeur par défaut “log”. Cela signifie que si nous voulons invoquer console.log avec cette fonction wrapper, nous n’avons pas besoin de spécifier level. Génial, n’est-ce pas ? Sauf que lorsque Babel transforme cette fonction, la sortie ressemble à ceci :


function logger(message) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";

  console[level](message);
}

Voici un exemple de comment, malgré nos meilleures intentions, le confort des développeurs peut se retourner contre nous. Ce qui n’était qu’une poignée d’octets dans notre code source a maintenant été transformé en un code de production beaucoup plus grand. L’uglification ne peut pas y faire grand-chose non plus, car les arguments ne peuvent pas être réduits. Oh, et si vous pensez que les rest parameters pourraient être un antidote valable, leurs transformations par Babel sont encore plus impressionnantes :


// Source
function logger(...args) {
  const [level, message] = args;

  console[level](message);
}

// Babel output
function logger() {
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key];
  }

  const level = args[0],
        message = args[1];
  console[level](message);
}

Pire encore, Babel transforme ce code même pour les projets avec une configuration @babel/preset-env ciblant les navigateurs actuels, ce qui signifie que les bundles actuels dans votre JavaScript servi de manière différenciée seront également affectés ! Vous pourriez utiliser des loose transforms pour réduire ça – et c’est une bonne idée, car ils sont souvent un peu plus petits que leurs homologues plus conformes aux spécifications – mais activer des loose transforms peut causer des problèmes si vous retirez Babel de votre circuit de build plus tard.

Que vous décidiez ou non d’activer les loose transforms, voici une façon de passer à travers la couche des paramètres par défaut transpilés :


// Babel won't touch this
function logger(message, level) {
  console[level || "log"](message);
}

Bien sûr, les paramètres par défaut ne sont pas les seuls dont il faut se méfier. Par exemple, la spread syntax est transformée, tout comme les fonctions arrow et beaucoup d’autres choses.

Si vous ne voulez pas éviter complètement ces éléments, vous avez plusieurs façons de réduire leur impact :

  1. Si vous créez une librairie, pensez à utiliser @babel/runtime avec @babel/plugin-transform-runtime pour dédoubler les fonctions d’aide que Babel met dans votre code.
  2. Pour les fonctionnalités polyfill dans les applications, vous pouvez ponctuellement les inclure avec @babel/polyfill via @babel/preset-env’s useBuiltIns : option “usage”.

C’est uniquement mon opinion, mais je crois que le meilleur choix est d’éviter le transpile total dans les bundles générés pour les navigateurs actuels. Ce n’est pas toujours possible, surtout si vous utilisez JSX, qui doit être transformé pour tous les navigateurs, ou si vous utilisez des fonctionnalités de langage qui ne sont pas largement supportées. Dans ce dernier cas, il peut être utile de se demander si ces fonctionnalités sont vraiment nécessaires pour offrir une bonne expérience utilisateur (elles le sont rarement). Si vous arrivez à la conclusion que Babel doit faire partie de votre chaîne de compilation, alors il vaut prendre la peine de temps en temps de jeter un coup d’œil sous le capot pour voir ce que Babel pourrait faire de non-optimal que vous pourriez améliorer.

L’amélioration n’est pas une course

Alors que vous massez vos tempes en vous demandant quand cette horrible gueule de bois JavaScript va s’en aller, comprenez que c’est précisément quand nous nous précipitons pour obtenir quelque chose aussi vite que possible, que l’expérience utilisateur souffre. Puisque le milieu du développement web est obsédé par l’itération plus rapide au nom de la concurrence, cela vaut la peine de ralentir un peu. Vous constaterez qu’en faisant ça, vous n’irez peut-être pas aussi vite que vos concurrents, mais votre produit sera plus rapide que le leur.

Au fur et à mesure que vous appliquerez ces suggestions à votre code source, sachez que les progrès ne se feront pas spontanément du jour au lendemain. Le développement web est un travail. Le travail qui a le plus d’impact se fait lorsque nous sommes réfléchis et dévoués à la fabrication sur le long terme. Concentrez-vous sur des améliorations constantes. Mesurez, testez, répétez, et l’expérience utilisateur de votre site s’améliorera, et vous gagnerez en rapidité petit à petit avec le temps.

Remerciements spéciaux à Jason Miller pour l’édition technique de cet article. Jason est le créateur et l’un des nombreux responsables de Preact, une alternative beaucoup plus petite à React avec la même API. Si vous utilisez Preact, pensez à supporter Preact via Open Collective.

Jeremy Wagner
Jeremy Wagner est un consultant en web performance qui fait de son mieux pour rendre le web plus rapide. Il est également l'auteur de The WebP Manual, sur le Smashing Magazine.

Un coup de fil ?

Composez votre numéro de téléphone ;
par exemple : 0612314567.

La mise en relation est
automatique et gratuite !