Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using @babel/runtime-corejs2 and @babel/runtime-corejs3 leads to larger bundle sizes #9853

Open
GeorgeTaveras1231 opened this issue Apr 12, 2019 · 23 comments

Comments

@GeorgeTaveras1231
Copy link

GeorgeTaveras1231 commented Apr 12, 2019

Bug Report

Current Behavior
When configuring @babel/plugin-transform-runtime to use corejs2 or corejs3, bundle sizes increase x7 (corejs2) and x11 (corejs3)

Input Code

const obj = {
  a: 1,
  b: 2,
};

const { b, ...rest } = obj;
const newObj = {...rest, a: b };

Expected behavior/code
I expect that the bundle sizes stay relatively close, when switching from @babel/runtime to @babel/runtime-corejs2 or @babel/runtime-corejs3

Babel Configuration (.babelrc, package.json, cli command)

Configs to use @babel/runtime

module.exports = {
  presets: [
    ["@babel/preset-env", {
      modules: false,
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: [
    "@babel/plugin-transform-runtime"
  ]
}

Configs to use @babel/runtime-corejs2

module.exports = {
  presets: [
    ["@babel/preset-env", {
      modules: false,
      useBuiltIns: 'usage',
      corejs: 2
    }]
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", {
      corejs: 2
    }]
  ]
}

Configs to use @babel/runtime-corejs3

module.exports = {
  presets: [
    ["@babel/preset-env", {
      modules: false,
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", {
      corejs: 3
    }]
  ]
}

Environment

  • Babel version(s): 7.4.3 (all babel packages)
  • Node/npm version: Node v10
  • OS: OSX 10.13.6
  • Monorepo: no
  • How you are using Babel: CLI, Webpack loader

Possible Solution

N/A

Additional context/Screenshots
Add any other context about the problem here. If applicable, add screenshots to help explain.

Repro repo: https://github.com/GeorgeTaveras1231/corejs-size-regression-repro

Snapshot:

Screen Shot 2019-04-12 at 11 41 52 AM

@babel-bot
Copy link
Collaborator

Hey @GeorgeTaveras1231! We really appreciate you taking the time to report an issue. The collaborators
on this project attempt to help as many people as possible, but we're a limited number of volunteers,
so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack
community
that typically always has someone willing to help. You can sign-up here
for an invite.

@zloirock
Copy link
Member

At least, you should not use together useBuiltIns option of preset-env and corejs option of transform-runtime.

However, it's not the main reason for increasing the size of the bundle. objectWithoutProperties helper for the "rest" operator internally use Object.getOwnPropertySymbols method, so runtime should load full Symbol polyfill for that. In those helpers also used some other methods which should be polyfilled. So it's expected.

@GeorgeTaveras1231
Copy link
Author

@zloirock I understand that objectWithoutProperties leads to loading of the full Symbol polyfill, but does that only happen in @babel/runtime-corejs2&@babel/runtime-corejs3. If so, then that may justify the bundle size increase; otherwise, it's still not clear to me what is causing it.

@zloirock
Copy link
Member

zloirock commented Apr 12, 2019

In your example, without @babel/runtime-corejs2 / @babel/runtime-corejs3 it just will not work in old engines because for injection required polyfills by useBuiltIns you should transpile helpers in @babel/runtime.

@dlong500
Copy link

dlong500 commented Jun 10, 2019

@zloirock I'm experiencing something related to this and I'm not seeing a clear explanation here.

With useBuiltIns: false and @babel/polyfill in my webpack entry array then my main bundle is 693kb.
Then, I remove @babel/polyfill and:
With useBuiltIns: "usage" and corejs: 2 my main bundle is 700kb.
With useBuiltIns: "usage" and corejs: 3 my main bundle is 706kb.

Other vendor bundles have a similar 10-20kb size increase as well.

Sure it's not a huge difference, but I thought the usage option was specifically designed to reduce bundle size. Why are both builds with the usage setting larger than the old style build using @babel/polyfill? And why is the core-js 3 build larger than core-js 2?

I should mention that in all three scenarios my app works fine in the older browsers I've specified, so it's not an issue of polyfills not being included in some of the builds.

@nicolo-ribaudo
Copy link
Member

What are your preset-env targets?

@dlong500
Copy link

"targets": {
  "browsers": ["> 0.5%", "last 2 versions", "ie >= 10", "edge >= 12", "firefox >= 50", "chrome >= 50"]
}

@radum
Copy link

radum commented Jun 11, 2019

In your example, without @babel/runtime-corejs2 / @babel/runtime-corejs3 it just will not work in old engines because for injection required polyfills by useBuiltIns you should transpile helpers in @babel/runtime.

@zloirock What do you mean by you should transpile helpers in @babel/runtime. If we use @babel/runtime-corejs3 we shouldn't use useBuiltIns?

I'm not following why is that the case?

As I see it as per the docs from babel-plugin-transform-runtime:

The plugin defaults to assuming that all polyfillable APIs will be provided by the user. Otherwise the corejs option needs to be specified.

So the size of the bundle above is smaller because the polyfillable APIs have not been provied by the user. When we specifiy the corejs version it will be done automatically by us.

But how does this relate to babel-preset-env useBuiltIns option?

@zloirock
Copy link
Member

If we use @babel/runtime-corejs3 we shouldn't use useBuiltIns?

Yep. They are for the same - injection polyfills on usage, but do it in different ways - runtime without global pollution, useBuiltIns with.

@dlong500
Copy link

dlong500 commented Jun 11, 2019

At least, you should not use together useBuiltIns option of preset-env and corejs option of transform-runtime.

Are you saying we shouldn't be using transform-runtime at all with useBuiltIns or only the corejs option of transform-runtime?

I'm still experimenting with different combinations and keep getting different bundle sizes, so I'm trying to figure out what exactly is the best config (assuming there aren't bugs causing increased bundle size in certain situations).

I did come across RFC #10008 which seems very interesting. In my particular case here I'm OK with global pollution if it means lower bundle size as this is an app, not a library. After getting some clarification I'll report back with my bundle sizes under the different configurations.

@zloirock
Copy link
Member

@dlong500 only transform-runtime with corejs option. However, for correct work useBuiltIns should also inject polyfills to helpers from transform-runtime which makes configuration harder.

Global polyfills could be larger, but they are more correct.

@dlong500
Copy link

However, for correct work useBuiltIns should also inject polyfills to helpers from transform-runtime which makes configuration harder.

Sorry for being dense. Can you clarify what you mean above? Am I missing some documentation that exists on using useBuiltIns with transform-runtime or is this stuff just too new?

And finally, what would be your general recommendation at this point for what options use for an application (not library) being bundled with webpack.

@zloirock
Copy link
Member

zloirock commented Jun 12, 2019

transform-runtime injects helpers and some helpers depends on globals which should be polyfilled. Since helpers will be injected by the link and their body will be in node_modules, if you use useBuiltIns: usage, you should somehow transpile @babel/runtime dependency. But since it could cause circular dependencies, you should be careful in configuration.

useBuiltIns + runtime for helpers is preferable way for applications. However, I recommend useBuiltIns: entry with required parts of core-js since it's much simpler in configuration and much more predictable. useBuiltIns: usage makes sense only for small applications and, for correct work, much harder in configuration.

karlguillotte added a commit to avalanche-canada/ac-web that referenced this issue Jul 12, 2019
Remove "useBuiltIns" as it is not required anymore.
babel/babel#9853
@pleunv
Copy link

pleunv commented Nov 5, 2019

Sorry... What?

@babel babel deleted a comment from SeasonsNeedfulThings Nov 5, 2019
@babel babel deleted a comment from SeasonsNeedfulThings Nov 5, 2019
@babel babel deleted a comment from SeasonsNeedfulThings Nov 5, 2019
@babel babel deleted a comment from SeasonsNeedfulThings Nov 5, 2019
@babel babel deleted a comment from pleunv Nov 5, 2019
@JMarkoski
Copy link
Contributor

JMarkoski commented Apr 26, 2020

I will try to clarify in details about what happens exactly and why.

First, core-js exposes two packages that are relevant in this context and they are: core-js and core-js-pure. Now, core-js defines global polyfills, and core-js-pure provides polyfills that don't pollute the global environment. What this means in simpler words is that if you write this import "core-js/stable/set";, you import from the package that defines polluting polyfills, and as a result you have global.Set. On the other hand, if you write this import Set from "core-js-pure/stable/set";, you avoid polluting the global environment, you import Set and bundle it with your app.

Now, what does useBuiltIns: 'usage' together with the corejs option set on @babel/preset-env do? If you have var p = new Promise() in your app, babel will transform it to something like this:

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var p = new Promise();

The 'usage' word suggests what babel does: If you use code that browsers you targeted with your browser configuration do not support, babel will inject polyfills for that code. But the main question is: from where?
If we inspect the transpiled code, we see that babel injects the Promise polyfill from core-js. Note that you should have core-js installed as a dependency in your app. Babel injects the polyfills that pollute the global environment with this setup.

Ok, now let's see what @babel/transform-runtime does with the corejs option set. If you have the same code above, that is var p = new Promise(), babel transpiles that to somethings like this:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var p = new _promise["default"]();

As we can see, the imports come from @babel/runtime-corejs3. This babel package doesn't contain any source code. Instead it only lists core-js and regenerator-runtime as dependencies. The @babel/transform-runtime plugin does some magic and inserts several folders with files in this package in node_modules. This is why @babel/runtime-corejs3/helpers or @babel/runtime-corejs3/core-js-stable folders exist in the first place. Now, the important part is that the files in these folders import things from core-js-pure. So when you use @babel/transform-runtime plugin, it includes polyfills from the core-js-pure folder, and doesn't pollute the global environment.

Answer to the question:

Why when using @babel/transform-runtime my app gets large?

As explained above, your app code is bundled together with the polyfills for features like Promise. Important to note here is that @babel/transform-runtime doesn't care about your targets. It doesn't even have an option, to specify the targets, it just includes the polyfill even if you want to target new environments.

Answer to the question:

Why with using useBuiltIns: "usage" on @babel/preset-env the code is smaller than when using @babel/transform-runtime?

As explained above, with this config, polyfills are included from the core-js package that pollutes global environment, and in the end you get something like global.Promise. Important to note here is that @babel/preset-env respects your targets, and doesn't include unnecessary pollyfills, where @babel/transform-runtime will include every polyfillable feature, which leads to many unnecessary polyfills.

Answer to the question:

Should I use useBuiltIns: 'usage' and corejs option on @babel/preset-env together with @babel/transform-runtime with core-js option set to false?

The answer is NO. This is not obvious at first. The only case where you will get away with this usage is when you include @babel/runtime-corejs3 or @babel/runtime-corejs2 in your app, and that is when corejs: *! And not when you include @babel/runtime, which happens when corejs: false. Why? To explore this, we need a different example. Let's say you have this code in your app:

async function f() {}

The transpiled code is this:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

require("regenerator-runtime/runtime");

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

function f() {
  return _f.apply(this, arguments);
}

function _f() {
  _f = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _f.apply(this, arguments);
}

Look at this line here:

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

Babel includes helpers from @babel/runtime! These helpers can depend on some global features to be available. In this case, that feature is Promise. The code in @babel/runtime/helpers/asyncToGenerator uses Promises!. Now you may think: But with useBuiltIns: 'usage' I included polyfills for my targeted browsers?. Yes, that's true, but with that config babel includes polyfills when you use that feature in your code! And you haven't used Promise anywhere! Now you have a problem. You need a way to transpile the babel helpers, and that's not good. This is the case that @zloirock refers to, when he says that you need to transpile the helpers. You will get away here if you use Promise in your app, because then globally pollutable polyfill for Promise will be injected like this: require("core-js/modules/es6.promise"); As you can see, this is not predictable, and very difficult to configure. Note about the cases where I said that you will get away if you have @babel/runtime-corejs3 or @babel/runtime-corejs2 as dependencies instead of @babel/runtime. In this case polyfills from core-js-pure will be injected, like this: var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator")); (corejs option should be set on @babel/transform-runtime as well)

Answer to the question:

What is a good config then?

I'll go like this:

App: If you are authoring an app, use import 'core-js at the top of your app with useBuiltIns set to entry and @babel/transform-runtime only for helpers (@babel/runtime as dependency). This way you pollute the global environment but you don't care, its your app. You will have the benefit of helpers aliased to @babel/runtime and polyfills included at the top of your app. This way you also don't need to process node_modules (except when a dependency uses a syntax that has to be transpiled) because if some dependency used a feature that needs a polyfill, you already included that polyfill at the top of your app.

Library: If you are authoring a library, use only @babel/transform-runtime with corejs option plus @babel/runtime-corejs3 as dependency, and @babel/preset-env for syntax transpilation with useBuiltIns: false. Also I would transpile packages I would use from node_modules. For this you will need to set the absoluteRuntime option (https://babeljs.io/docs/en/babel-plugin-transform-runtime#absoluteruntime) to resolve the runtime dependency from a single place, because @babel/transform-runtime imports from @babel/runtime-corejs3 directly, but that only works if @babel/runtime-corejs3 is in the node_modules of the file that is being compiled.

This is my understanding, @zloirock , @nicolo-ribaudo feel free to correct me if I am wrong somewhere. Also feel free to ask if something is unclear from the explanations.

Useful links:

#10008
https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
https://babeljs.io/docs/en/babel-plugin-transform-runtime
#10271

Thanks for reading!

@radum
Copy link

radum commented Apr 26, 2020

@JMarkoski Thank you very much for this. My understanding of how babel and core-js work is much clear now.

Here is another comment around the same topic #9728

PS: Also your issue links are wrong.

@JMarkoski
Copy link
Contributor

JMarkoski commented Apr 26, 2020

Oh, I messed up the links. They are fixed now.

PS: You are welcome, I'm glad if it helped.

@Zardddddd60
Copy link

Zardddddd60 commented Jun 11, 2020

@JMarkoski Thank you for your explanation! !

Library: If you are authoring a library, For this you will need to set the absoluteRuntime option (https://babeljs.io/docs/en/babel-plugin-transform-runtime#absoluteruntime) to resolve the runtime dependency from a single place,

I try to set this option to true or a relative path when authoring a library, finding the compiled file importing runtime and polyfill from a path like:

import _regeneratorRuntime from "/absolute-path-of-my-pc/node_modules/@babel/runtime-corejs3/regenerator";

I don't think this library can be used on other environments.
How to set this absoluteRuntime option?


EDIT:

There is an example using absoluteRuntime -- @vue/babel-preset-app

@elyobo
Copy link

elyobo commented Jul 15, 2020

@JMarkoski that is a fantastic rundown; is there an equivalent somewhere in the docs that I'm missing? It's really unclear how these options interoperate and what the correct approach should be.

@JMarkoski
Copy link
Contributor

JMarkoski commented Aug 11, 2020

I'm sorry for responding late guys. @elyobo Thanks, I'm glad if my answer helped. I'm not aware of such explanation in the docs/issues, my understanding comes from reading the source code of babel/core-js and trying different configurations and seeing how they behave. I was motivated to write on the topic exactly because there wasn't any explanation I could find anywhere on how exactly the options work together and what happens exactly and why. The docs explain them but you don't get the full idea of how the options play together. It would be nice if we add detailed explanations in order to clarify their usage, because I've seen improper usage of them in many projects/tutorials, which leads to not so easy to debug errors if you are not very familiar with how slightly different configs of the tools you use affect your code.

mwiencek added a commit to mwiencek/musicbrainz-server that referenced this issue Sep 2, 2020
This is very confusing stuff, but this conflicts with `useBuiltIns:
'usage' on preset-env, which already imports (global-polluting) core-js
polyfills based on their usage *and* our target browsers. Whereas
plugin-transform-runtime inserts "pure" (non-global-polluting) polyfills
but does *not* care about our target browsers (it inserts them for
everything). This has the effect that we were importing core-js
polyfills twice: once for the global versions inserted by preset-env
based on usage, and once for the pure versions from
plugin-transform-runtime.

More details can be found in this helpful comment by Jovica Markoski:
babel/babel#9853 (comment)

However, I didn't follow his suggestion to use `useBuiltIns: 'entry'`
with a single core-js import at the top, because while this does only
import polyfills needed by our target browsers, it doesn't do that based
on /usage/. It imports any polyfill potentially needed by our supported
browsers, which includes a ton we don't use and bloats the build size.

So I'm still using the 'usage' setting here, but I've added
@babel/runtime to the babel-ignored exceptions list so that the helper
utilities receive polyfills too.

React is also added as an exception to the babel-ignored list, because
the "react" and "react-dom" libraries must share the same Symbol
polyfill to avoid issues like
facebook/react#8379.
@shmup
Copy link

shmup commented Jan 5, 2021

@JMarkoski your comment above is excellent. the division on "making an app? a library?" is what kinda just makes it Obvious, the solution. i'm sure in hindsight i could read the relevant docs again, and possibly make sense of it myself... tho i'm not 100% sure ;)

thanks for the speedy understanding

mostly made this a comment to say: really might be worth clarifying this confusion in yet Another way in babel docs, because i think many people will go down a searching-hole for this

@mryechkin
Copy link

@JMarkoski holy crap, thank you for that excellent explanation!! 👏🏻

I had spent the last 2 days banging my head against the wall trying to understand how all this ties together - your comment made everything so much more clear!

@nyngwang
Copy link

To resolve the problem, which is also mentioned there:

[...] And you haven't used Promise anywhere! Now you have a problem.

You can also include @babel/runtime into your code, e.g. when defining babel-loader in webpack.config.js:

{
  test: /\.jsx?$/,
  exclude: {
    and: [/node_modules/],
    not: [
      /@babel[\\/]runtime/,  // <---- include it by not exclude it.
    ],
  },
  use: {
    loader: 'babel-loader',
    options: {
      // omitted.
    }
  }
}

Now @babel/runtime will get polyfilled too.

But here is a caveat: Say @babel/runtime itself has a dependency that uses some ES6+ features, and you have not included it into not: [...], then these features will not get polyfilled. So the config above is not future-proof.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.