Es ist schwer vorstellbar, produktionsreifen JavaScript-Code ohne ein Tool wie Babel zu schreiben. Es hat das Spiel verändert und modernen Code für eine breite Palette von Benutzern zugänglich gemacht. Da diese Herausforderung größtenteils bewältigt ist, hält uns nicht mehr viel davon ab, uns wirklich auf die Funktionen zu konzentrieren, die moderne Spezifikationen zu bieten haben.
Aber gleichzeitig wollen wir uns nicht zu sehr hineinsteigern. Wenn Sie gelegentlich einen Blick auf den Code werfen, den Ihre Benutzer tatsächlich herunterladen, werden Sie feststellen, dass scheinbar einfache Babel-Transformationen manchmal besonders aufgebläht und komplex sein können. Und in vielen dieser Fälle können Sie dieselbe Aufgabe mit einem einfachen, "altmodischen" Ansatz ausführen – ohne das schwere Gepäck, das aus der Vorverarbeitung resultieren kann.
Werfen wir einen genaueren Blick darauf, was ich meine, indem wir die Online-REPL von Babel verwenden – ein großartiges Werkzeug zum schnellen Testen von Transformationen. Wir zielen auf Browser ab, die ES2015+ nicht unterstützen, und werden damit nur einige der Fälle hervorheben, in denen es sich für Sie (und Ihre Benutzer) lohnt, einen "altmodischen" Weg zu wählen, um etwas in JavaScript zu tun, trotz eines "neuen" Ansatzes, der durch moderne Spezifikationen populär gemacht wird.
Behalten Sie dabei im Hinterkopf, dass es weniger um "alt gegen neu" geht, sondern vielmehr darum, die beste Implementierung zu wählen, die die Aufgabe erfüllt und gleichzeitig unerwartete Nebeneffekte unserer Build-Prozesse umgeht.
Legen wir los!
Vorverarbeitung einer for..of-Schleife
Die for..of-Schleife ist ein flexibles, modernes Mittel zum Iterieren über iterierbare Sammlungen. Sie wird oft ähnlich wie eine traditionelle for-Schleife verwendet, was Sie dazu verleiten könnte zu denken, dass Babels Transformation einfach und vorhersehbar wäre, besonders wenn Sie sie nur mit einem Array verwenden. Nicht ganz. Der Code, den wir schreiben, mag nur 98 Bytes sein
function getList() {
return [1, 2, 3];
}
for (let value of getList()) {
console.log(value);
}
Aber die Ausgabe ergibt 1,8 KB (eine Steigerung von 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();
}
Warum wurde dafür nicht einfach eine for-Schleife verwendet? Es ist ein Array! Anscheinend weiß Babel in diesem Fall nicht, dass es sich um ein Array handelt. Alles, was es weiß, ist, dass es mit einer Funktion arbeitet, die jedes iterierbare Element (Array, String, Objekt, NodeList) zurückgeben könnte, und es muss auf alles vorbereitet sein, was dieser Wert sein könnte, basierend auf der ECMAScript-Spezifikation für die for..of-Schleife.
Wir könnten die Transformation drastisch reduzieren, indem wir explizit ein Array übergeben, aber das ist in einer realen Anwendung nicht immer einfach. Um also die Vorteile von Schleifen (wie break- und continue-Anweisungen) zu nutzen und gleichzeitig die Bundle-Größe schlank zu halten, greifen wir vielleicht einfach zur for-Schleife. Sicher, sie ist altmodisch, aber sie erledigt die Aufgabe.
function getList() {
return [1, 2, 3];
}
for (var i = 0; i < getList().length; i++) {
console.log(getList()[i]);
}
/explanation Dave Rupert hat vor ein paar Jahren über genau diese Situation geschrieben und festgestellt, dass forEach, selbst mit Polyfill, für ihn eine gute Lösung war.
Vorverarbeitung von Array [...Spread]
Ähnliche Sache hier. Der Spread-Operator kann mit mehr als einer Klasse von Objekten (nicht nur Arrays) verwendet werden. Wenn Babel also nicht weiß, mit welcher Art von Daten es zu tun hat, muss es Vorsichtsmaßnahmen treffen. Leider können diese Vorsichtsmaßnahmen zu einer erheblichen Byte-Aufblähung führen.
Hier ist die Eingabe mit schlanken 81 Bytes
function getList () {
return [4, 5, 6];
}
console.log([1, 2, 3, ...getList()]);
Die Ausgabe bläht sich auf 1,3 KB auf
"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())));
Stattdessen könnten wir direkt zur Sache kommen und einfach concat() verwenden. Der Unterschied in der Menge des zu schreibenden Codes ist nicht signifikant, er tut genau das, was er tun soll, und man muss sich keine Sorgen über die zusätzliche Aufblähung machen.
function getList () {
return [4, 5, 6];
}
console.log([1, 2, 3].concat(getList()));
Ein häufigeres Beispiel: Iterieren über eine NodeList
Das haben Sie vielleicht schon öfter gesehen. Wir müssen oft mehrere DOM-Elemente abfragen und über die resultierende NodeList iterieren. Um forEach auf dieser Sammlung verwenden zu können, ist es üblich, sie in ein Array zu spreizen.
[...document.querySelectorAll('.my-class')].forEach(function (node) {
// do something
});
Aber wie wir gesehen haben, führt dies zu einer starken Ausgabe. Als Alternative ist nichts falsch daran, diese NodeList über eine Methode des Array-Prototyps wie slice laufen zu lassen. Gleiches Ergebnis, aber viel weniger Ballast.
[].slice.call(document.querySelectorAll('.my-class')).forEach(function(node) {
// do something
});
Hinweis zu "Loose"-Modus
Es ist erwähnenswert, dass ein Teil dieser Array-bezogenen Aufblähung auch durch die Nutzung des Loose-Modus von @babel/preset-env vermieden werden kann, der Kompromisse bei der strikten Einhaltung der Semantik von modernem ECMAScript eingeht, aber den Vorteil einer schlankeren Ausgabe bietet. In vielen Situationen kann das gut funktionieren, aber Sie führen auch zwangsläufig Risiken in Ihre Anwendung ein, die Sie später vielleicht bereuen werden. Schließlich sagen Sie Babel damit, einige ziemlich kühne Annahmen darüber zu treffen, wie Sie Ihren Code verwenden.
Die wichtigste Erkenntnis hier ist, dass es manchmal besser ist, bewusster mit den Funktionen umzugehen, die Sie verwenden, anstatt mehr Zeit in die Feinabstimmung Ihres Build-Prozesses zu investieren und möglicherweise mit ungesehenen Konsequenzen später zu kämpfen.
Vorverarbeitung von Standardparametern
Dies ist eine besser vorhersehbare Operation, aber wenn sie wiederholt in einer Codebasis verwendet wird, können sich die Bytes ansammeln. ES2015 führte Standardwerte für Parameter ein, die die Signatur einer Funktion bereinigen, wenn sie optionale Argumente akzeptiert. Hier sind wir bei 75 Bytes
function getName(name = "my friend") {
return `Hello, ${name}!`;
}
Aber Babel kann mit seiner Transformation etwas ausführlicher sein als erwartet, was zu 169 Bytes führt
"use strict";
function getName() {
var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "my friend";
return "Hello, ".concat(name, "!");
}
Als Alternative könnten wir die Verwendung des arguments-Objekts ganz vermeiden und einfach prüfen, ob ein Parameter undefined ist. Wir verlieren die selbsterklärende Natur, die Standardparameter bieten, aber wenn wir wirklich Bytes sparen wollen, könnte es sich lohnen. Und je nach Anwendungsfall könnten wir sogar damit durchkommen, auf falsey zu prüfen, um es noch weiter zu verschlanken.
function getName(name) {
name = name || "my friend";
return `Hello, ${name}!`;
}
Vorverarbeitung von async/await
Der syntaktische Zucker von async/await über die Promise API ist eine meiner Lieblingserweiterungen in JavaScript. Dennoch kann Babel es aus dem Stand heraus ganz schön verhunzen.
157 Bytes zum Schreiben
async function fetchSomething(url) {
const response = await fetch(url);
return await response.json();
}
fetchSomething("https://google.com");
1,5 KB nach der Kompilierung
"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");
Sie werden feststellen, dass Babel async-Code nicht standardmäßig in Promises umwandelt. Stattdessen werden sie in Generatoren umgewandelt, die auf die regenerator-runtime-Bibliothek angewiesen sind, was zu viel mehr Code führt, als in unserer IDE geschrieben wird. Glücklicherweise ist es möglich, den Promise-Weg über ein Plugin wie babel-plugin-transform-async-to-promises zu gehen. Anstelle der Ausgabe von 1,5 KB erhalten wir deutlich weniger, nämlich 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);
}
};
}
Aber wie bereits erwähnt, birgt die Abhängigkeit von einem Plugin zur Linderung solcher Probleme Risiken. Wenn wir dies tun, beeinflussen wir Transformationen im gesamten Projekt und führen außerdem eine weitere Build-Abhängigkeit ein. Stattdessen könnten wir erwägen, einfach bei der Promise API zu bleiben.
function fetchSomething(url) {
return fetch(url).then(function (response) {
return response.json();
}).then(function (data) {
return resolve(data);
});
}
Vorverarbeitung von Klassen
Für mehr syntaktischen Zucker gibt es die class-Syntax, die mit ES2015 eingeführt wurde und eine optimierte Möglichkeit bietet, die prototypische Vererbung von JavaScript zu nutzen. Aber wenn wir Babel zum Transpilieren für ältere Browser verwenden, gibt es nichts Süßes an der Ausgabe.
Die Eingabe belässt uns nur bei 120 Bytes
class Robot {
constructor(name) {
this.name = name;
}
speak() {
console.log(`I'm ${this.name}!`);
}
}
Aber die Ausgabe ergibt 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;
}();
Meistens ist es, es sei denn, Sie betreiben eine ziemlich komplexe Vererbung, einfach genug, um einen pseudoklassischen Ansatz zu verwenden. Er erfordert etwas weniger Code zum Schreiben und die resultierende Schnittstelle ist praktisch identisch mit einer Klasse.
function Robot(name) {
this.name = name;
this.speak = function() {
console.log(`I'm ${this.name}!`);
}
}
const rob = new Robot("Bob");
rob.speak(); // "Bob"
Strategische Überlegungen
Beachten Sie, dass je nach Zielgruppe Ihrer Anwendung vieles von dem, was Sie hier lesen, bedeuten könnte, dass Ihre Strategien zur Verkleinerung von Bundles unterschiedliche Formen annehmen.
Zum Beispiel könnte Ihr Team bereits eine bewusste Entscheidung getroffen haben, die Unterstützung für Internet Explorer und andere "Legacy"-Browser einzustellen (was immer häufiger vorkommt, da die überwiegende Mehrheit der Browser ES2015+ unterstützt). In diesem Fall wäre Ihre Zeit am besten damit verbracht, die Liste der von Ihrem Build-System unterstützten Browser zu überprüfen oder sicherzustellen, dass Sie keine unnötigen Polyfills versenden.
Und selbst wenn Sie ältere Browser noch unterstützen müssen (oder vielleicht lieben Sie einige der modernen APIs zu sehr, um sie aufzugeben), gibt es andere Optionen, die es Ihnen ermöglichen, schwere, vorverarbeitete Bundles nur an die Benutzer zu liefern, die sie benötigen, wie z. B. eine differenzielle Bereitstellung-Implementierung.
Wichtig ist nicht so sehr, welche Strategie (oder Strategien) Ihr Team zu priorisieren wählt, sondern vielmehr, diese Entscheidungen bewusst im Lichte des Codes zu treffen, der von Ihrem Build-System ausgespuckt wird. Und das alles beginnt damit, dass man das Dist-Verzeichnis aufschlägt, um einen Blick hineinzuwerfen.
Werfen Sie einen Blick unter die Haube
Ich bin ein großer Fan der neuen Funktionen, die modernes JavaScript weiterhin bietet. Sie machen Anwendungen leichter zu schreiben, zu warten, zu skalieren und insbesondere zu lesen. Aber solange das Schreiben von JavaScript die Vorverarbeitung von JavaScript bedeutet, ist es wichtig, sicherzustellen, dass wir den Puls dessen fühlen, was diese Funktionen für die Benutzer bedeuten, denen wir letztendlich dienen wollen.
Und das bedeutet, ab und zu den Motorhaube Ihres Build-Prozesses zu öffnen. Im besten Fall können Sie besonders aufwändige Babel-Transformationen vermeiden, indem Sie eine einfachere, "klassische" Alternative verwenden. Und im schlimmsten Fall werden Sie die Arbeit, die Babel leistet, umso besser verstehen (und schätzen).
Aber die Ausgabe ergibt 1,8 KB (eine Steigerung von 1736 %!) Es sollte ein Hinweis darauf geben, dass Sie nicht jedes Mal 1,8 KB JavaScript hinzufügen, wenn Sie eine for..off-Schleife schreiben.
Es ist immer gut zu wissen, wie unsere Tools im Hintergrund funktionieren, aber wenn mir jemand sagen würde, dass er async/await nicht verwendet, weil es 1,5 KB JavaScript beim Kompilieren hinzufügt, würde ich mich wahrscheinlich erschießen.
Interessant. Ich meine, sicher, ich wusste immer, dass die Transformation von modernem Code für ältere Browser einen Preis hat, aber ich wusste nicht, wie viel er kostet.
Andererseits könnte mein üblicher Ansatz zu diesem Thema heißen: "Vermeiden Sie schwere Babel-Transformationen, indem Sie (so oft wie möglich) keinen JavaScript-Code schreiben".
Ich neige immer dazu, so viel JavaScript wie möglich aus den Projekten zu entfernen, an denen ich arbeite. Ich meine, der schnellste Code ist immer der, der nicht existiert, oder?
Aber die Einbeziehung der Tipps aus diesem Artikel kann die Leistung sicherlich noch ein wenig verbessern, daher bin ich dankbar für die Einsicht.
Dieser Artikel ist wirklich aufschlussreich, aber es ist auch erwähnenswert, dass, wenn Sie mehrere Transformationen durchführen, diese transformierten Code teilen können.
Das allgemeine Thema stimmt aber: Kennen Sie Ihre Werkzeuge und die Kosten, die Sie damit verursachen!
Interessante Lektüre, danke!
Im letzten Beispiel zu Klassen gibt es einen Tippfehler: Die Ausgabe ergibt 989 Bytes, nicht Kilobytes (Gott sei Dank!).
Die Ausgabe von 989 KB sind Bytes, nehme ich an.
Vielen Dank für den informativen Artikel. Da wir tiefer in die 2020er Jahre vordringen, hoffe ich, dass bald der Tag kommt, an dem fast alle Entwickler die Unterstützung für sehr alte Legacy-Browser einstellen können. Dies wird uns helfen, Codebasen, die viel leichter sind, an die überwiegende Mehrheit der Benutzer mit Browsern, die nach 2017 oder so veröffentlicht wurden, zu liefern...
In Bezug auf "Looping over a NodeList". Ich glaube, Sie meinten "HTMLCollection". NodeList-Sammlungen haben standardmäßig die Methode "forEach()". Daher ist es nicht notwendig, "querySelectorAll()"-Auswahlen in ein Array zu spreizen, nur um die "forEach"-Methode zu verwenden, da sie bereits enthalten ist.
Mit "HTMLCollection" haben Sie jedoch kein "forEach()" zur Verfügung, daher müssen Sie diese in ein Array spreizen, wenn Sie "forEach()" verwenden möchten. Eine "HTMLCollection" wird erstellt, wenn Sie die älteren "getElementsByClassName()" und "getElementsByTagName()" verwenden.
Ich denke, die im Fall von
for-ofvorgeschlagene Lösung ist tatsächlich nicht korrekt? Sie sollte wahrscheinlich so aussehenSie sagen also, ich sollte nur
varverwenden, richtig?Tolle Lektüre! Phänomenale Einsichten, ich habe noch nie über das Gewicht der Verwendung neuer Features nachgedacht, wenn ich es nicht brauche.
Ihr Promises-Beispiel kann noch weiter vereinfacht werden
Es ist keine explizite
Promise-Umhüllung erforderlich, dafetchbereits eine zurückgibt.Toller Artikel! Ein möglicher mildernder Faktor: Ein Großteil der hier beschriebenen Aufblähung stammt von den Polyfill-Funktionen, die Babel hinzufügt, um die ES6-Konstrukte zu unterstützen. Da diese Funktionsdefinitionen nur einmal hinzugefügt werden und viele davon über mehrere ES6-Konstrukte geteilt werden, wird der Anteil der Aufblähung schnell abnehmen, je häufiger die Konstrukte verwendet werden.
Sehr guter Punkt.
Dennoch ein großartiger Artikel.
Ausgezeichnet! Tolle Arbeit! Gibt es ein Online-Tool, um die Größe des generierten Bundles oder der Datei zu überprüfen?
Entwicklertools können eine gute Option für die Dateigröße sein. Gibt es eine andere Option anstelle von?
Danke :)
Was auch immer, Mann... Ich bleibe bei es2015. Wir laufen nicht mehr mit 64 MB RAM.
Obwohl informativ, ist die Art und Weise, wie der Artikel formuliert ist, für neue Entwickler irreführend. Der Größenunterschied ist für reale Anwendungen vernachlässigbar.
Abgesehen davon wäre ich daran interessiert zu erfahren, wie sich der for..of-Fall im Vergleich zum TypeScript-Compiler verhält, da der Typ bekannt wäre.