Every web developer likes cool ES6+ features: generators, iterables, async-await and others. What may be wrong using them?
Bad old browsers
Sad, but people still use old browsers. And I'm not talking specifically of IE here — some people just turn off auto-update on their mobile phones and don't care anymore. Indeed it's sad 😥
Should I care?
If you just develop some app — it depends. You know your users better; perhaps they are technically advanced and simply don't use old browsers. Or perhaps IE users fraction is small and non-paying, so you can disregard it completely.
But if you are authoring a JS lib — you definitely should. For the moment, libs are usually distributed transpiled to ES5 so they can work in any environment (however, it's assumed it's ok to require polyfills).
So, let's see what JS features turn your nice-looking ES6+ code into large and bloated ES5!
1. Generators
Perhaps the most famous ES5-hostile construct. It's so prominent that Airbnb has a separate note on it.
input
function* gen() {
yield 1
yield 2
}
TypeScript output
var __generator = /* Somewhat long helper function */
function gen() {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, 1];
case 1:
_a.sent();
return [4 /*yield*/, 2];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}
Good news about TypeScript: there is a way to define helper functions like __generator
once per bundle. However, the generator definition is always translated to a finite automata that doesn't look as nice as the source 😕
Babel output
require("regenerator-runtime/runtime.js");
var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen);
function gen() {
return regeneratorRuntime.wrap(function gen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;
case 2:
_context.next = 4;
return 2;
case 4:
case "end":
return _context.stop();
}
}
}, _marked);
}
Babel goes even further — it moves all generators runtime to a different module. Which is, unfortunately, quite large 🐘
What to do?
Use iterables. But be cautious — there's a way to bloat your code with them as well 😉
2. async-await
What? Isn't it just a syntax sugar over Promises? Let's see!
input
export async function fetchExample() {
const r = await fetch('https://example.com')
return await r.text();
}
TypeScript output
var __awaiter = /* Some convoluted JS code */
var __generator = /* We saw it already! */
function fetchExample() {
return __awaiter(this, void 0, void 0, function () {
var r;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, fetch('https://example.com')];
case 1:
r = _a.sent();
return [4 /*yield*/, r.text()];
case 2: return [2 /*return*/, _a.sent()];
}
});
});
}
It's even worse than generators! async-await
is in fact a generator which additionally suspends on Promises.
Babel output
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("regenerator-runtime/runtime.js");
function asyncGeneratorStep/* Like __awaiter */
function _asyncToGenerator/* Yet another converter */
function fetchExample() {
return _fetchExample.apply(this, arguments);
}
function _fetchExample() {
_fetchExample = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
var r;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return fetch('https://example.com');
case 2:
r = _context.sent;
_context.next = 5;
return r.text();
case 5:
return _context.abrupt("return", _context.sent);
case 6:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _fetchExample.apply(this, arguments);
}
Babel thinks of async-await
just like TypeScript does: they are generators with some additional stuff, so it produces not only it imports, but some helper functions as well.
What to do?
Use simple Promises chains. While they may look too “traditional”, they transpile well to anything.
3. Iterables iteration
Multiple JS constructs cause iterators iteration: for-of
loop, iterables spread, and iterables destructuring.
However, there are some good news about this feature:
-
TypeScript: without
downlevelIteration
the compiler 1) allows only arrays iteration, and 2) transpiles iteration to simple indexed access - Babel: if the compiler infers array it uses simple indexed access
However, if these news do not apply to your code, it's getting bloated 😐
input
const iterable = (() => [1, 2])()
for (const i of iterable) {
console.log(i)
}
TypeScript output
var __values = /* ... */
var e_1, _a;
var iterable = (function () { return [1, 2]; })();
try {
for (var iterable_1 = __values(iterable), iterable_1_1 = iterable_1.next(); !iterable_1_1.done; iterable_1_1 = iterable_1.next()) {
var i = iterable_1_1.value;
console.log(i);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (iterable_1_1 && !iterable_1_1.done && (_a = iterable_1.return)) _a.call(iterable_1);
}
finally { if (e_1) throw e_1.error; }
}
There's a special handling for the case if iterable
is a generator. It's not needed for our example but the compiler can't be sure.
Babel output
function _createForOfIteratorHelper/* ... */
function _unsupportedIterableToArray/* ... */
function _arrayLikeToArray/* ... */
var iterable = function () {
return [1, 2];
}();
var _iterator = _createForOfIteratorHelper(iterable),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var i = _step.value;
console.log(i);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
Just like TS, Babel handles exception case which is, in fact, not needed in this example.
What to do
- Don't iterate anything but arrays
- Otherwise, write a simple function:
function forEach(iterable, effect) {
const itr = iterable[Symbol.iterator]()
for ( ; ; ) {
const n = itr.next()
if (n.done) {
return n.value
}
effect(n.value)
}
}
Are there other bloaters?
Honestly, any ES6+ feature produces some additional code; however, as far as I know, the produced code is not as large as in examples above.
What to do?
Just read whatever your compiler produces and think if you can do something about it 🙂
When I looked into a dist
of my project for the first time I got shocked: almost every file had __read
or __whatever
, and all neat for-of
s were turned into large and ugly structures. However, having applied techniques I described here and there, I reduced the bundle size for about 15%. So can you! 😉
Thanks for reading this. Can you name some other bundle bloaters?