Evite transformaciones pesadas de Babel (a veces) no escribiendo JavaScript moderno

Índice
  1. Preprocesamiento de un bucle for…of
  2. Matriz de preprocesamiento […difusión]
  3. Parámetros predeterminados de preprocesamiento
  4. Preprocesamiento asíncrono/espera
  5. Clases de preprocesamiento
  6. Consideraciones estratégicas
  7. Pop open that hood

Es difícil imaginar escribir JavaScript listo para producción sin una herramienta como Babel . Ha sido un indiscutible punto de inflexión al hacer que el código moderno sea accesible para una amplia gama de usuarios. Una vez superado este desafío, no hay mucho que nos impida aprovechar las características que las especificaciones modernas tienen para ofrecer.

Pero al mismo tiempo, no queremos inclinarnos demasiado. Si echa un vistazo ocasional al código que sus usuarios están descargando, notará que a veces, las transformaciones de Babel aparentemente sencillas pueden ser especialmente infladas y complejas. Y en muchos de esos casos, puede realizar la misma tarea utilizando un enfoque simple, de la “vieja escuela”, sin el pesado equipaje que puede surgir del preprocesamiento.

Echemos un vistazo más de cerca a lo que estoy hablando usando REPL en línea de Babel , una gran herramienta para probar transformaciones rápidamente. Dirigido a navegadores que no soportan ES2015+, lo usaremos para resaltar solo algunas de las ocasiones en las que usted (y sus usuarios) podrían estar mejor eligiendo una forma “de la vieja escuela” de hacer algo en JavaScript, a pesar de una “nueva ”Enfoque popularizado por las especificaciones modernas.

A medida que avanzamos, tenga en cuenta que se trata menos de “viejo versus nuevo” y más de elegir la mejor implementación que haga el trabajo y evite los efectos secundarios esperados de nuestros procesos de compilación.

¡Construyamos!

Preprocesamiento de un bucle for…of

El for..ofbucle es un medio flexible y moderno de recorrer colecciones iterables. A menudo se usa de una manera muy similar a un forbucle tradicional, lo que puede llevarte a pensar que la transformación de Babel sería simple y predecible, especialmente si solo la estás usando con una matriz. No exactamente. El código que escribimos puede tener solo 98 bytes:

function getList() {  return [1, 2, 3];}for (let value of getList()) {  console.log(value);}

Pero el resultado es de 1,8 kb (¡un aumento del 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();}

¿Por qué no se utilizó simplemente el bucle for para esto? ¡Es una matriz! Aparentemente, en este caso, Babel no sabe que está manejando una matriz. Todo lo que sabe es que está trabajando con una función que podría devolver cualquier iterable (matriz, cadena, objeto, NodeList), y necesita estar listo para cualquier valor que pueda ser, según la especificación ECMAScript para el bucle for..of. .

Podríamos reducir drásticamente la transformación pasándole explícitamente una matriz, pero eso no siempre es fácil en una aplicación real. Por lo tanto, para aprovechar los beneficios de los bucles (como las instrucciones break y continue), mientras mantenemos el tamaño del paquete reducido con confianza, podríamos recurrir al bucle for. Claro, es de la vieja escuela, pero hace el trabajo.

function getList() {  return [1, 2, 3];}
for (var i = 0; i  getList().length; i++) {  console.log(getList()[i]);}

/explicación Dave Rupert escribió en su blog sobre esta situación exacta hace unos años y descubrió que forEach, incluso el polirelleno, era una buena solución para él.

Matriz de preprocesamiento […difusión]

Oferta similar aquí. El operador de dispersión se puede utilizar con más de una clase de objetos (no solo con matrices), por lo que cuando Babel no es consciente del tipo de datos con el que está tratando, debe tomar precauciones. Desafortunadamente, esas precauciones pueden provocar una grave sobrecarga de bytes.

Aquí está la entrada, con un peso de 81 bytes:

function getList () {  return [4, 5, 6];}
console.log([1, 2, 3, ...getList()]);

La salida aumenta a 1,3 kb:

"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())));

En su lugar, podríamos ir al grano y simplemente usar concat(). La diferencia en la cantidad de código que necesita escribir no es significativa, hace exactamente lo que debe hacer y no hay necesidad de preocuparse por esa sobrecarga adicional.

function getList () {  return [4, 5, 6];}
console.log([1, 2, 3].concat(getList()));

Un ejemplo más común: recorrer una lista de nodos

Es posible que hayas visto esto más de unas cuantas veces. A menudo necesitamos consultar varios elementos DOM y recorrer el resultado NodeList. Para utilizarlo forEachen esa colección, es común distribuirlo en una matriz.

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

Pero como vimos, esto genera una gran producción. Como alternativa, no hay nada de malo en ejecutar eso NodeLista través de un método en el Arrayprototipo, como slice. Mismo resultado, pero mucho menos equipaje:

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

Una nota sobre el modo “suelto”

Vale la pena señalar que parte de esta sobrecarga relacionada con la matriz también se puede evitar aprovechando el modo suelto@babel/preset-env de , que compromete mantenerse totalmente fiel a la semántica del ECMAScript moderno, pero ofrece el beneficio de una salida más delgada. En muchas situaciones, eso podría funcionar bien, pero también necesariamente está introduciendo un riesgo en su aplicación del que podría arrepentirse más adelante. Después de todo, le estás diciendo a Babel que haga algunas suposiciones bastante audaces sobre cómo estás usando tu código.

La principal conclusión aquí es que, a veces, podría ser más adecuado ser más intencional con respecto a las funciones que se van a utilizar, en lugar de invertir más tiempo en ajustar el proceso de construcción y potencialmente luchar con consecuencias invisibles más adelante.

Parámetros predeterminados de preprocesamiento

Esta es una operación más predecible, pero cuando se usa repetidamente en todo el código base, los bytes pueden acumularse. ES2015 introdujo valores de parámetros predeterminados, que ordenan la firma de una función cuando acepta argumentos opcionales. Aquí estamos en 75 bytes:

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

Pero Babel puede ser un poco más detallado de lo esperado con su transformación, lo que da como resultado 169 bytes:

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

Como alternativa, podríamos evitar el uso del argumentsobjeto por completo y simplemente verificar si un parámetro es. undefinedPerdemos la naturaleza de autodocumentación que brindan los parámetros predeterminados, pero si realmente estamos recortando bytes, podría valer la pena. Y dependiendo del caso de uso, es posible que incluso podamos salirnos con la nuestra comprobando para falseyadelgazarlo aún más.

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

Preprocesamiento asíncrono/espera

El azúcar sintáctico de async/awaitover the Promise API es una de mis adiciones favoritas a JavaScript. Aun así, desde el primer momento, Babel puede hacer un gran desastre.

157 bytes para escribir:

async function fetchSomething(url) {  const response = await fetch(url);  return await response.json();}fetchSomething("https://google.com");

1,5 KB cuando se compila:

"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");

Notarás que Babel no convierte asyncel código en promesas listas para usar. En cambio, se transforman en generadores que dependen de la regenerator-runtimebiblioteca, generando mucho más código del que está escrito en nuestro IDE. Afortunadamente, es posible seguir la ruta de la Promesa mediante un complemento, como babel-plugin-transform-async-to-promises. En lugar de esa salida de 1,5 kb, terminamos con mucho menos, 638 bytes:

"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);    }  };}

Pero, como se mencionó antes, existe un riesgo al confiar en un complemento para aliviar dolores como este. Al hacerlo, impactamos las transformaciones en todo el proyecto y también introducimos otra dependencia de compilación. En su lugar, podríamos considerar simplemente seguir con la API de Promise.

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

Clases de preprocesamiento

Para obtener más azúcar sintáctico, está la classsintaxis introducida con ES2015, que proporciona una forma simplificada de aprovechar la herencia prototípica de JavaScript. Pero si usamos Babel para transpilar navegadores más antiguos, el resultado no tiene nada de bueno.

El input nos deja sólo 120 bytes:

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

Pero el resultado es de 989 bytes:

"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 mayor parte del tiempo, a menos que esté haciendo una herencia bastante complicada, es bastante sencillo utilizar un enfoque pseudoclásico . Requiere un poco menos de código para escribir y la interfaz resultante es prácticamente idéntica a una clase.

function Robot(name) {  this.name = name;
  this.speak = function() {    console.log(`I'm ${this.name}!`);  }}
const rob = new Robot("Bob");rob.speak(); // "Bob"

Consideraciones estratégicas

Tenga en cuenta que, dependiendo de la audiencia de su aplicación, mucho de lo que está leyendo aquí podría significar que sus estrategias para mantener los paquetes reducidos pueden adoptar diferentes formas.

Por ejemplo, es posible que su equipo ya haya tomado la decisión deliberada de dejar de admitir Internet Explorer y otros navegadores “heredados” (lo cual se está volviendo cada vez más común, dado que la gran mayoría de los navegadores admiten ES2015+). Si ese es el caso, lo mejor sería dedicar su tiempo a auditar la lista de navegadores a los que se dirige su sistema de compilación o a asegurarse de no enviar polyfills innecesarios.

And even if you are still obligated to support older browsers (or maybe you love some of the modern APIs too much to give them up), there are other options to enable you to ship heavy, preprocessed bundles only to the users that need them, like a differential serving implementation.

The important thing isn’t so much about which strategy (or strategies) your team chooses to prioritize, but more about intentionally making those decisions in light of the code being spit out by your build system. And that all starts by cracking open that dist directory to take a peak.

Pop open that hood

I’m a big fan of the new features modern JavaScript continues to provide. They make for applications that are easier to write, maintain, scale, and especially read. But as long as writing JavaScript means preprocessing JavaScript, it’s important to make sure that we have a finger on the pulse of what these features mean for the users that we ultimately aim to serve.

And that means popping the hood of your build process once in a while. At best, you might be able avoid especially hefty Babel transformations by using a simpler, “classic” alternative. And at worst, you’ll come to better understand (and appreciate) the work that Babel does all the more.

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion