Catégories
Astuces et Design

Évitez les transformations Babel lourdes en n'écrivant pas (parfois) du JavaScript moderne

Il est difficile d'imaginer écrire du JavaScript prêt pour la production sans un outil comme Babel. Cela a révolutionné le jeu en rendant le code moderne accessible à un large éventail d'utilisateurs. Avec ce défi en grande partie à l'écart, il n'y a pas grand-chose qui nous empêche de vraiment nous pencher sur les fonctionnalités que les spécifications modernes ont à offrir.

Mais en même temps, nous ne voulons pas trop nous pencher. Si vous jetez un coup d'œil occasionnel au code que vos utilisateurs téléchargent réellement, vous remarquerez que parfois, des transformations Babel apparemment simples peuvent être particulièrement gonflées et complexes. Et dans beaucoup de ces cas, vous pouvez effectuer la même tâche en utilisant une approche simple, «old school» – sans pour autant les bagages lourds qui peuvent provenir du prétraitement.

Examinons de plus près ce dont je parle en utilisant REPL en ligne de Babel – un excellent outil pour tester rapidement les transformations. Ciblant les navigateurs qui ne prennent pas en charge ES2015 +, nous l'utiliserons pour mettre en évidence quelques-unes des fois où vous (et vos utilisateurs) pourrait il vaut mieux choisir une méthode «old school» pour faire quelque chose en JavaScript, malgré une «nouvelle» approche popularisée par les spécifications modernes.

Au fur et à mesure, gardez à l’esprit qu’il s’agit moins de «vieux contre nouveau» que de choisir le meilleur mise en œuvre qui fait le travail tout en contournant les effets secondaires attendus de nos processus de construction.

Construisons!

Prétraitement d'une boucle for..of

le for..of loop est un moyen flexible et moderne de parcourir des collections itérables. Il est souvent utilisé d'une manière très similaire à un traditionnel for , ce qui peut vous amener à penser que la transformation de Babel serait simple et prévisible, surtout si vous l'utilisez simplement avec un tableau. Pas assez. Le code que nous écrivons ne peut être que de 98 octets:

function getList() {
  return (1, 2, 3);
}

for (let value of getList()) {
  console.log(value);
}

Mais la sortie se traduit par 1,8 Ko (une augmentation de 1736%!):


"use strict";

function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o(Symbol.iterator) == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o(i++) }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.nIn order to be iterable, non-array objects must have a (Symbol.iterator)() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o(Symbol.iterator)(); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2(i) = arr(i); } return arr2; }

function getList() {
  return (1, 2, 3);
}

var _iterator = _createForOfIteratorHelper(getList()),
    _step;

try {
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
    var value = _step.value;
    console.log(value);
  }
} catch (err) {
  _iterator.e(err);
} finally {
  _iterator.f();
}

Pourquoi n'a-t-il pas simplement utilisé la boucle pour cela? C’est un tableau! Apparemment, dans ce cas, Babel ne fait pas savoir il gère un tableau. Tout ce qu'il sait, c'est qu'il fonctionne avec une fonction qui pourrait renvoyer tout itérable (tableau, chaîne, objet, NodeList), et il doit être prêt pour quelle que soit cette valeur, sur la base de la spécification ECMAScript pour la boucle for..of.

Nous pourrions considérablement réduire la transformation en lui passant explicitement un tableau, mais ce n'est pas toujours facile dans une application réelle. Ainsi, pour tirer parti des avantages des boucles (comme les instructions break and continue), tout en gardant la taille du paquet en toute confiance, nous pourrions juste atteindre la boucle for. Bien sûr, c'est de la vieille école, mais ça fait le travail.

function getList() {
  return (1, 2, 3);
}


for (var i = 0; i < getList().length; i++) {
  console.log(getList()(i));
}

/ explication Dave Rupert a blogué sur cette situation exacte il y a quelques années et a trouvé que forEach, même polyfilled, était une bonne solution pour lui.

Tableau de prétraitement (… Spread)

Accord similaire ici. L'opérateur d'étalement peut être utilisé avec plusieurs classes d'objets (pas juste tableaux), donc lorsque Babel n'est pas au courant du type de données avec lesquelles il traite, il doit prendre des précautions. Malheureusement, ces précautions peuvent entraîner des ballonnements graves.

Voici l'entrée, pesant à peine 81 octets:

function getList () {
  return (4, 5, 6);
}


console.log((1, 2, 3, ...getList()));

Les ballons de sortie à 1,3 Ko:

"use strict";

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }

function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.nIn order to be iterable, non-array objects must have a (Symbol.iterator)() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }

function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2(i) = arr(i); } return arr2; }

function getList() {
  return (4, 5, 6);
}

console.log((1, 2, 3).concat(_toConsumableArray(getList())));

Au lieu de cela, nous pourrions aller droit au but et simplement utiliser concat(). La différence dans la quantité de code que vous devez écrire n'est pas significative, elle fait exactement ce qu'elle est censée faire, et il n'y a pas lieu de s'inquiéter de cette surcharge supplémentaire.

function getList () {
  return (4, 5, 6);
}


console.log((1, 2, 3).concat(getList()));

Un exemple plus courant: boucle sur une NodeList

Vous l'avez peut-être vu plus d'une fois. Nous devons souvent interroger plusieurs éléments DOM et boucler sur le résultat NodeList. Pour utiliser forEach sur cette collection, il est courant de la diffuser dans un tableau.

(...document.querySelectorAll('.my-class')).forEach(function (node) {
  // do something
});

Mais comme nous l'avons vu, cela donne une sortie importante. Comme alternative, il n'y a rien de mal à exécuter ce NodeList grâce à une méthode sur la Array prototype, comme slice. Même résultat, mais beaucoup moins de bagages:

().slice.call(document.querySelectorAll('.my-class')).forEach(function(node) {
  // do something
});

Une note sur le mode «lâche»

Il convient de souligner que certains de ces ballonnements liés à la matrice peuvent également être évités en tirant parti @babel/preset-envLe mode lâche, qui compromet en restant totalement fidèle à la sémantique d'ECMAScript moderne, mais offre l'avantage d'une sortie plus mince. Dans de nombreuses situations, cela peut très bien fonctionner, mais vous introduisez également nécessairement un risque dans votre application que vous pourriez regretter plus tard. Après tout, vous dites à Babel de faire des hypothèses plutôt audacieuses sur la façon dont vous utilisez votre code.

Le principal point à retenir ici est que, parfois, il pourrait être plus approprié d'être plus intentionnel au sujet des fonctionnalités que vous utilisez, plutôt que d'investir plus de temps pour peaufiner votre processus de construction et potentiellement lutter contre des conséquences invisibles plus tard.

Pré-traitement des paramètres par défaut

Il s'agit d'une opération plus prévisible, mais lorsqu'elle est utilisée à plusieurs reprises dans une base de code, les octets peuvent s'additionner. ES2015 a introduit des valeurs de paramètres par défaut, qui nettoient la signature d'une fonction lorsqu'elle accepte des arguments facultatifs. Nous voici à 75 octets:

function getName(name = "my friend") {
  return `Hello, ${name}!`;
}

Mais Babel peut être un peu plus verbeux que prévu avec sa transformation, résultant en 169 octets:

"use strict";


function getName() {
  var name = arguments.length > 0 && arguments(0) !== undefined ? arguments(0) : "my friend";
  return "Hello, ".concat(name, "!");
}

Comme alternative, nous pourrions éviter d’utiliser le arguments objet tout à fait, et vérifiez simplement si un paramètre est undefined Nous perdons la nature d'auto-documentation fournie par les paramètres par défaut, mais si nous sommes vraiment en train de pincer des octets, cela en vaut la peine. Et selon le cas d'utilisation, nous pourrions même être en mesure de falsey pour l'amincir encore plus.

function getName(name) {
  name = name || "my friend";
  return `Hello, ${name}!`;
}

Prétraitement asynchrone / attente

Le sucre syntaxique de async/await sur l'API Promise est l'un de mes ajouts préférés à JavaScript. Même ainsi, hors de la boîte, Babel peut en faire tout un gâchis.

157 octets pour écrire:

async function fetchSomething(url) {
  const response = await fetch(url);
  return await response.json();
}

fetchSomething("https://google.com");

1,5 Ko lors de la compilation:

"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen(key)(arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

function fetchSomething(_x) {
  return _fetchSomething.apply(this, arguments);
}

function _fetchSomething() {
  _fetchSomething = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(url) {
    var response;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return fetch(url);

          case 2:
            response = _context.sent;
            _context.next = 5;
            return response.json();

          case 5:
            return _context.abrupt("return", _context.sent);

          case 6:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _fetchSomething.apply(this, arguments);
}

fetchSomething("https://google.com");

Vous remarquerez que Babel ne convertit pas async code en promesses hors de la boîte. Au lieu de cela, ils sont transformés en générateurs qui dépendent de la regenerator-runtime bibliothèque, ce qui génère beaucoup plus de code que ce qui est écrit dans notre IDE. Heureusement, il est possible de suivre la route Promise au moyen d'un plugin, comme babel-plugin-transform-async-to-promises. Au lieu de cette sortie de 1,5 Ko, nous nous retrouvons avec beaucoup moins, à 638 octets:

"use strict";


function _await(value, then, direct) {
  if (direct) {
    return then ? then(value) : value;
  }


  if (!value || !value.then) {
    value = Promise.resolve(value);
  }


  return then ? value.then(then) : value;
}


var fetchSomething = _async(function (url) {
  return _await(fetch(url), function (response) {
    return _await(response.json());
  });
});


function _async(f) {
  return function () {
    for (var args = (), i = 0; i < arguments.length; i++) {
      args(i) = arguments(i);
    }


    try {
      return Promise.resolve(f.apply(this, args));
    } catch (e) {
      return Promise.reject(e);
    }
  };
}

Mais, comme mentionné précédemment, il existe un risque de s'appuyer sur un plugin pour soulager la douleur comme celle-ci. Ce faisant, nous impactons les transformations du tout projet, et introduisant également une autre dépendance de génération. Au lieu de cela, nous pourrions envisager de simplement rester avec l'API Promise.

function fetchSomething(url) {
  return fetch(url).then(function (response) {
    return response.json();
  }).then(function (data) {
    return resolve(data);
  });
}

Classes de prétraitement

Pour plus de sucre syntaxique, il y a le class syntaxe introduite avec ES2015, qui fournit un moyen simplifié de tirer parti de l'héritage prototypique de JavaScript. Mais si nous utilisons Babel pour transpiler pour les navigateurs plus anciens, la sortie n'a rien de doux.

L'entrée nous seulement 120 octets:

class Robot {
  constructor(name) {
    this.name = name;
  }


  speak() {
     console.log(`I'm ${this.name}!`);
  }
}

Mais la sortie se traduit par 989 octets:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props(i); descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var Robot = /*#__PURE__*/function () {
  function Robot(name) {
    _classCallCheck(this, Robot);

    this.name = name;
  }

  _createClass(Robot, ({
    key: "speak",
    value: function speak() {
      console.log("I'm ".concat(this.name, "!"));
    }
  }));

  return Robot;
}();

La plupart du temps, à moins que vous ne fassiez un héritage assez complexe, il est assez simple d’utiliser une approche pseudoclassique. L'écriture nécessite un peu moins de code et l'interface résultante est pratiquement identique à une classe.

function Robot(name) {
  this.name = name;


  this.speak = function() {
    console.log(`I'm ${this.name}!`);
  }
}


const rob = new Robot("Bob");
rob.speak(); // "Bob"

Considérations stratégiques

Gardez à l'esprit que, selon l'audience de votre application, une grande partie de ce que vous lisez ici peut signifier que votre les stratégies pour garder les paquets minces peuvent prendre différentes formes.

Par exemple, votre équipe a peut-être déjà délibérément décidé de supprimer la prise en charge d'Internet Explorer et d'autres navigateurs «hérités» (ce qui devient de plus en plus courant, étant donné que la grande majorité des navigateurs prennent en charge ES2015 +). Si tel est le cas, il est préférable de consacrer votre temps à vérifier la liste des navigateurs ciblés par votre système de génération ou à vous assurer de ne pas envoyer de polyfills inutiles.

Et même si vous êtes toujours obligé de prendre en charge les navigateurs plus anciens (ou peut-être que vous aimez trop certaines des API modernes pour les abandonner), il existe d'autres options pour vous permettre d'expédier des bundles lourds et prétraités uniquement aux utilisateurs qui en ont besoin, comme une mise en œuvre différentielle.

L'important n'est pas tant la stratégie (ou les stratégies) que votre équipe choisit de prioriser, mais plutôt de prendre intentionnellement ces décisions à la lumière du code craché par votre système de build. Et tout commence par ouvrir ce répertoire dist pour prendre un pic.

Ouvrez ce capot

Je suis un grand fan des nouvelles fonctionnalités que JavaScript continue de proposer. Ils permettent des applications plus faciles à écrire, à maintenir, à mettre à l'échelle et surtout à lire. Mais tant que l'écriture de JavaScript signifie prétraitement JavaScript, il est important de s'assurer que nous avons le pouls de ce que ces fonctionnalités signifient pour les utilisateurs que nous visons finalement à servir.

Et cela signifie faire sauter le capot de votre processus de construction de temps en temps. Au mieux, vous pourriez éviter des transformations Babel particulièrement lourdes en utilisant une alternative plus simple et «classique». Et au pire, vous arriverez à mieux comprendre (et apprécier) le travail que Babel fait de plus en plus.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *