Aguarde...

26 de maio de 2020

Evite transformações pesadas de Babel (às vezes) não escrevendo JavaScript moderno

Evite transformações pesadas de Babel (às vezes) não escrevendo JavaScript moderno

É difícil imaginar escrever JavaScript pronto para produção sem uma ferramenta como Babel . Tem sido uma mudança de jogo indiscutível ao tornar o código moderno acessível a uma ampla gama de usuários. Com esse desafio em grande parte fora do caminho, não há muito o que nos impedir de realmente confiar nos recursos que as especificações modernas têm a oferecer.

Mas, ao mesmo tempo, não queremos nos apoiar demais. Se você der uma olhada ocasional no código que seus usuários estão realmente baixando, perceberá que, às vezes, transformações aparentemente simples de Babel podem ser especialmente inchadas e complexas. E, em muitos desses casos, você pode executar a mesma tarefa usando uma abordagem simples da “velha escola” – sem a bagagem pesada que pode advir do pré-processamento.

Vamos dar uma olhada no que estou falando usando o REPL online da Babel – uma ótima ferramenta para testar rapidamente transformações. Ao direcionar navegadores que não oferecem suporte ao ES2015 +, usaremos para destacar apenas algumas das vezes em que você (e seus usuários) pode estar melhor escolhendo uma maneira “antiga” de fazer algo em JavaScript, apesar de uma “nova ”Popularizada pelas especificações modernas.

À medida que avançamos, lembre-se de que se trata menos de “velho versus novo” e mais sobre a escolha da melhor implementação que faz o trabalho, evitando os efeitos colaterais esperados de nossos processos de compilação.

Vamos construir!

#Pré-processamento de um loop for..of

for..ofloop é um meio flexível e moderno de loop sobre coleções iteráveis. É frequentemente usado de maneira muito semelhante a um forloop tradicional , o que pode levar você a pensar que a transformação de Babel seria simples e previsível, especialmente se você estiver usando apenas com uma matriz. Não é bem assim. O código que escrevemos pode ter apenas 98 bytes:

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

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

Mas a saída resulta em 1,8 kb (um aumento 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();
}

Por que não usou apenas o loop for para isso? É uma matriz! Aparentemente, nesse caso, Babel não sabe que está lidando com uma matriz. Tudo o que sabe é que está trabalhando com uma função que pode retornar qualquer iterável (matriz, string, objeto, NodeList) e precisa estar pronto para qualquer valor que possa ser, com base na especificação ECMAScript para o loop for..of .

Poderíamos reduzir drasticamente a transformação passando explicitamente uma matriz para ela, mas isso nem sempre é fácil em um aplicativo real. Portanto, para aproveitar os benefícios dos loops (como instruções de interrupção e continuação), mantendo o tamanho do pacote com confiança, podemos alcançar o loop for. Claro, é da velha escola, mas faz o trabalho.

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


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

/ explicação Dave Rupert postou no blog sobre essa situação exata há alguns anos e descobriu que, para cada um, até polifilado, é uma boa solução para ele.

#Matriz de pré-processamento [… Spread]

Negócio semelhante aqui. O operador de propagação pode ser usado com mais de uma classe de objetos (e não apenas matrizes); portanto, quando Babel não está ciente do tipo de dados com o qual está lidando, precisa tomar precauções. Infelizmente, essas precauções podem resultar em inchaço grave de bytes.

Aqui está a entrada, pesando apenas 81 bytes:

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


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

A saída aumenta para 1.3kb:

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

Em vez disso, poderíamos ir direto ao ponto e usar concat()A diferença na quantidade de código que você precisa escrever não é significativa, faz exatamente o que se destina a fazer e não há necessidade de se preocupar com esse inchaço extra.

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


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

Um exemplo mais comum: loop sobre um NodeList

Você pode ter visto isso mais de algumas vezes. Geralmente, precisamos consultar vários elementos DOM e fazer um loop sobre o resultado NodeList. Para usar forEachnessa coleção, é comum espalhá-la em uma matriz.

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

Mas, como vimos, isso gera uma produção pesada. Como alternativa, não há nada errado em executar isso NodeListatravés de um método no Arrayprotótipo, como slice. Mesmo resultado, mas muito menos bagagem:

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

Uma observação sobre o modo “loose”

Vale ressaltar que parte desse inchaço relacionado à matriz também pode ser evitado com o uso @babel/preset-envdo modo loose , que compromete a fidelidade total à semântica do ECMAScript moderno, mas oferece o benefício de uma saída mais fina. Em muitas situações, isso pode funcionar bem, mas você também está necessariamente introduzindo riscos em seu aplicativo dos quais pode se arrepender mais tarde. Afinal, você está dizendo a Babel para fazer algumas suposições bastante ousadas sobre como você está usando seu código. 

O principal argumento aqui é que, às vezes, pode ser mais adequado ser mais intencional sobre os recursos que você usa, em vez de investir mais tempo em aprimorando seu processo de compilação e potencialmente lutando com consequências invisíveis posteriormente.

#Pré-processamento de parâmetros padrão

Esta é uma operação mais previsível, mas quando é usada repetidamente em uma base de código, os bytes podem aumentar. O ES2015 introduziu valores de parâmetro padrão, que organizam a assinatura de uma função quando ela aceita argumentos opcionais. Aqui estamos em 75 bytes:

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

Mas Babel pode ser um pouco mais detalhado do que o esperado com sua transformação, resultando em 169 bytes:

"use strict";


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

Como alternativa, poderíamos evitar o uso do argumentsobjeto por completo e simplesmente verificar se um parâmetro é undefinedPerdemos a natureza de auto-documentação que os parâmetros padrão fornecem, mas se estamos realmente comprimindo bytes, pode valer a pena. E, dependendo do caso de uso, podemos ser capazes de nos livrar falseyainda mais da verificação.

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

#Pré-processamento assíncrono / aguardado

O açúcar sintático de async/awaittodo o Promise API é uma das minhas adições favoritas para JavaScript. Mesmo assim, fora da caixa, Babel pode fazer disso uma bagunça.

157 bytes para escrever:

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

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

1.5kb quando compilado:

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

Você notará que Babel não converte asynccódigo em promessas imediatamente. Em vez disso, eles são transformados em geradores que dependem da regenerator-runtimebiblioteca, gerando muito mais código do que o que está escrito em nosso IDE. Felizmente, é possível seguir a rota do Promise por meio de um plugin, como babel-plugin-transform-async-to-promises. Em vez dessa saída de 1,5kb, acabamos com muito menos, em 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);
    }
  };
}

Mas, como mencionado anteriormente, há risco de contar com um plug-in para aliviar dores como essa. Ao fazer isso, estamos impactando transformações em todo o projeto e também introduzindo outra dependência de compilação. Em vez disso, poderíamos considerar apenas aderir à API do Promise.

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

#Classes de pré-processamento

Para obter mais açúcar sintático, há a classsintaxe introduzida no ES2015, que fornece uma maneira otimizada de aproveitar a herança prototípica do JavaScript. Mas se estivermos usando o Babel para transpilar para navegadores mais antigos, não há nada de bom na saída.

A entrada nos deixa apenas 120 bytes:

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


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

Mas a saída resulta em 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;
}();

Na maioria das vezes, a menos que você esteja fazendo uma herança bastante envolvida, é bastante simples usar uma abordagem pseudoclássica . Requer um pouco menos de código para escrever, e a interface resultante é praticamente idêntica a uma classe.

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


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


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

#Considerações estratégicas

Lembre-se de que, dependendo do público-alvo do seu aplicativo, muito do que você está lendo aqui pode significar que suas estratégias para manter os pacotes compactos magros podem ter formas diferentes.

Por exemplo, sua equipe já pode ter tomado uma decisão deliberada de descartar o suporte ao Internet Explorer e outros navegadores “legados” (o que está se tornando cada vez mais comum, considerando que a grande maioria dos navegadores oferece suporte ao ES2015 +). Se for esse o caso, é melhor gastar seu tempo na auditoria da lista de navegadores que seu sistema de criação está direcionando ou para garantir que você não esteja enviando polyfills desnecessários.

E mesmo que você ainda seja obrigado a oferecer suporte a navegadores mais antigos (ou talvez goste muito de algumas das APIs modernas), existem outras opções para permitir o envio de pacotes pesados ​​e pré-processados ​​apenas aos usuários que precisam deles, como uma implementação de veiculação diferencial .

O importante não é tanto sobre qual estratégia (ou estratégias) sua equipe escolhe priorizar, mas mais sobre tomar intencionalmente essas decisões à luz do código sendo cuspido por seu sistema de compilação. E tudo isso começa abrindo esse diretório dist para ter um pico.

#Abra esse capuz

Sou um grande fã dos novos recursos que o JavaScript moderno continua a fornecer. Eles criam aplicativos que são mais fáceis de escrever, manter, dimensionar e principalmente ler. Mas, desde que escrever JavaScript signifique pré-processar JavaScript, é importante ter uma noção do que esses recursos significam para os usuários que pretendemos servir.

E isso significa destacar o processo de construção de vez em quando. Na melhor das hipóteses, você poderá evitar transformações especialmente pesadas de Babel usando uma alternativa mais simples e “clássica”. E, na pior das hipóteses, você entenderá melhor (e apreciará) o trabalho que Babel faz ainda mais.

Postado em Blog
Escreva um comentário