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?
Top comments (2)
What to really do is just don't worry. This is no reason to limit yourself from using certain features.
ES5 only users are a small minority and while supporting them is important, worrying about bundle size for a small fraction of the userbase is unnecessary. Thankfully the nomodule and type="module" script tag attributes come to the rescue.
Just build both and have the ES5 bundle loaded in with the nomodule attribute and the newer one with the type="module" attribute on its script tag. A newer browser will not load the older one and an older one can't load the newer bundle. :)
In Angular 8+ the CLI does this automatically for you by default under the name of "Differential Loading"
It's completely true for applications. I agree that es5 bundle, if exist, must be the separate, and people using recent browsers must not download what they don't need.
However, it's different for libs which provide web runtime — they are still distributed as es5 or even es3 builds. So if you are authoring such a lib you have to keep that in mind and inspect your builds.