DEV Community

loading...

3 JavaScript features that bloat your ES5 bundle

alekseiberezkin profile image Aleksei Berezkin ・4 min read

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
}
Enter fullscreen mode Exit fullscreen mode

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*/];
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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()];
            }
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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-ofs 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?

Discussion (2)

pic
Editor guide
Collapse
alexaegis profile image
AlexAegis

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"

Collapse
alekseiberezkin profile image
Aleksei Berezkin Author

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.