-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
In the last three years and a half, @babel/preset-env
has shown its full potential in reducing bundle sizes not only by not transpiling supported syntax features, but also by not including unnecessary core-js
polyfills.
Currently Babel has three different ways to inject core-js
polyfills in the source code:
- By using
@babel/preset-env
'suseBuiltIns: "entry"
option, it is possible to inject polyfills for every ECMAScript functionality not natively supported by the target browsers; - By using
useBuiltIns: "usage"
, Babel will only inject polyfills for unsupported ECMAScript features but only if they are actually used in the input souce code; - By using
@babel/plugin-transform-runtime
, Babel will inject ponyfills (which are "pure" and don't pollute the global scope) for every used ECMAScript feature supported bycore-js
. This is usually used by library authors.
ℹ️ INFO: Before continuing, I highly recommend reading "Annex B: The current relationship between Babel and
core-js
" to get a deeper understanding of the current situation!
Our position in the JavaScript ecosystem allows us to push these optimizations even further. @babel/plugin-transform-runtime
has big advantages for some users over useBuiltIns
, but it doesn't consider target environments: it's 2020 and probably very few people need to load an Array.prototype.forEach
polyfill.
Additionally, why should we limit this ability to automatically inject only the necessary polyfill to core-js
? There are also DOM polyfills, Intl polyfills, and polyfills for a myriad of other web platform APIs. Additionally, not everyone wants to use core-js
: there are many other valid ECMAScript polyfills, which have different tradeoffs (e.g. source size vs spec compliancy) and may work better for some users.
What if the logic to inject them was not related to the actual data about the available or required polyfills, so that they can be used and developed independently?
Concepts
-
Polyfill provider is a special kind of Babel plugin that injects is used to specify which JavaScript expressions need to be polyfilled, where to load that polyfill from and how to apply it. Multiple polyfill providers can be used at the same time, so that users can load, for example, both an ECMAScript polyfill provider and a DOM-related one.
Polyfill providers can expose three different methods of injecting the polyfills:
entry-global
, which reflects the currentuseBuiltIns: "entry"
option of@babel/preset-env
;usage-global
, which reflects the currentuseBuiltIns: "usage"
option of@babel/preset-env
;usage-pure
, which reflects the current polyfilling behavior of@babel/plugin-transform-runtime
.
Every interested project should have their own polyfill provider: for example,
babel-plugin-polyfill-corejs3
or@ungap/babel-plugin-polyfill
.
New user experience
Suppose a user is testing some ECMAScript proposals, they are localizing their application using Intl
and they are using fetch
. To avoid loading too many bytes of polyfills, they are ok with supporting only commonly used browsers, and they don't want to load unused polyfills.
How would their new config look like?
{
"presets": [
["@babel/env", { "targets": [">1%"] }]
],
"plugins": [
"@babel/proposal-class-properties",
["polyfill-corejs3", {
"targets": [">1%"],
"method": "usage-global",
"proposals": true
}]
]
}
New developer experience
In order to provide consistent APIs and functionalities to our users, we will provide utilities to:
- centralize the responsibility of handling the possible configuration options to a single shared package
- abstract the AST structure from the polyfill information, so that new usage detection features will be added to all the different providers.
We can provide those APIs in a new@babel/helper-define-polyfill-provider
package.
These new APIs will look like this:
import definePolyfillProvider from "@babel/helper-define-polyfill-provider";
export default definePolyfillProvider((api, options) => {
return {
name: "object-entries-polyfill-provider",
polyfills: {
"Object/entries": { chrome: "54", firefox: "47" },
},
entryGlobal(meta, utils, path) {
if (name !== "object-entries-polyfill") return false;
if (api.shouldInjectPolyfill("Object/entries")) {
utils.injectGlobalImport("object-entries-polyfill/global");
}
path.remove();
},
usageGlobal(meta, utils) {
if (
meta.kind === "property" &&
meta.placement === "static" &&
meta.object === "Object" &&
meta.property === "entries" &&
api.shouldInjectPolyfill("Object/entries")
) {
utils.injectGlobalImport("object-entries-polyfill/global");
}
},
usagePure(name, targets, path) {
if (
meta.kind === "property" &&
meta.placement === "static" &&
meta.object === "Object" &&
meta.property === "entries" &&
api.shouldInjectPolyfill("Object/entries")
) {
path.replaceWith(
utils.injectDefaultImport("object-entries-polyfill/pure")
);
}
}
}
});
The createPolyfillProvider
function will take a polyfll plugin factory, and wrap it to create a proper Babel plugin. The factory function takes the same arguments as any other plugin: an instance of an API object and the polyfill options.
It's return value is different from the object returned by normal plugins: it will have an optional method for each of the possible polyfill implementations. We won't disallow other keys in that object, so that we will be able to easily introduce new kind of polyfills, like "inline"
.
Every polyfilling method will take three parameters:
- A
meta
object describing the built-in to be polyfilled:Promise
->{ kind: "global", name: "Promise" }
Promise.try
->{ kind: "property", object: "Promise", key: "try", placement: "static" }
[].includes
->{ kind: "property", object: "Array", key: "includes", placement: "prototype" }
foo().includes
->{ kind: "property", object: null, key: "includes", placement: null }
- An
utils
object, exposing a few methods to inject imports in the current program. - The
NodePath
of the expression which triggered the polyfill provider call. It could be anImportDeclaration
(forentry-global
), an identifier (or computed expression) representing the method name, or aBinaryExpression
for"foo" in Bar
checks.
Polyfill providers will be able to specify custom visitors (like normal plugins): for exapmle, core-js
needs to inject some polyfills for yield*
expressions.
How does this affect the current plugins?
Implementing this RFC won't require changing any of the existing plugins.
We can start working on it as an experiment (like it was done for @babel/preset-env
), and wait to see how the community reacts to it.
If it will then success, we should integrate it in the main Babel project. It should be possible to do this without breaking changes:
- Remove the custom
useBuiltIns
implementation from@babel/preset-env
, and delegate to this plugin:- If
useBuiltIns
is enabled,@babel/preset-env
will enable@babel/inject-polyfills
- Depending on the value of the
corejs
option, it will inject the correct polyfill provider plugin.
- If
- Deprecate the
regenerator
andcorejs
options from@babel/plugin-transform-runtime
: both should be implemented in their own polyfill providers.
Open questions
Should the polyfill injector be part ofNo.@babel/preset-env
?- Pro: it's easier to share the
targets
. On the other hand, it would be more complex but feasible even if it was a separate plugin. - Con: it wouldn't be possible to inject polyfills without using
@babel/preset-env
. Currently@babel/plugin-transform-runtime
has this capability. - If it will be only available in the preset,
@babel/helper-polyfill-provider
should be a separate package. Otherwise, we can just export the needed hepers from the plugin package. - It could be a separate plugin, but
@babel/preset-env
could include it.
- Pro: it's easier to share the
- Who should maintain polyfill providers?
- Probably not us, but we shouldn't completely ignore them. It's more important to know the details of the polyfill rather than Babel's internals.
- Currently the
core-js
polyfilling logic in@babel/preset-env
and@babel/plugin-transform-runtime
has been mostly maintained by Denis (@zloirock). - It could be a way of attracting new contributors; both for us and for the polyfills maintainers.
- Both the
regenerator
andcore-js
providers should probably be in the Babel org (or at least supported by us), since we have been officially supporting them so far.
- Are Babel helpers similar to a polyfill, or we should not reuse the same logic for both?
Annex A: The ideal evolution after this proposal
This proposal defines polyfill providers without requiring any change to the current Babel architecture. This is an important point, because will allow us to experiment more freely.
However, I think that to further improve the user experience we should:
- Lift the
targets
option to the top-level configuration. By doing so, it can be shared by different plugins, presets or polyfill providers. - Add a new
polyfills
option, similar to the existingpresets
andplugins
.
By doing so, the above configuration would become like this:
{
"targets": [">1%"],
"presets": ["@babel/env"],
"plugins": ["@babel/proposal-class-properties"],
"polyfills": [
["corejs3", {
"method": "usage-global",
"proposals": true
}]
]
}
# Annex B: The current relationship between Babel and core-js
Babel is a compiler, core-js
is a polyfill.
A compiler is used to make modern syntax work in old browsers; a polyfill is used to make modern native functions work in old browsers. You usually want to use both, so that you can write modern code and run it in old browsers without problems.
However, compilers and polyfills are two independent units. There are a lot of compiler you can choose (Babel, TypeScript, Traceur, swc, ...), and a lot of polyfills you can choose (core-js
, es-shims
, polyfill.io
, ...).
You can choose them independently, but for historical reasons (and because core-js
is a really good polyfill!) so far Babel has made it easier to use core-js
.
What does the Babel compiler do with core-js
?
Babel internally does not depend on core-js
. What it does is providing a simple way of automatically generate imports to core-js
in your code.
Babel provides a plugin to generate imports to core-js
in your code. It's then your code that depends on core-js
.
// input (your code):
Symbol();
// output (your compiled code):
import "core-js/modules/es.symbol";
Symbol();
In order to generate those imports, we don't need to depend on core-js
: we handle your code as if it was a simple string, similarly to how this function does:
function addCoreJSImport(input) {
return `import "core-js/modules/es.symbol";\n` + input;
}
(well, it's not that simple! 😛)
Be careful though: even if Babel doesn't depend on core-js
, your code will do!
Is there any other way in which Babel directly depends on core-js
?
Kind of. While the Babel compiler itself doesn't depend on core-js
, we provide a few runtime packages that might be used at runtime by your application that depend on it.
@babel/polyfill
is a "proxy package": all what it does is importingregenerator-runtime
(a runtime helper used for generators) andcore-js
2. This package has been deprecated at least since the release ofcore-js
3 in favor of the direct inclusion of those two other packages.
One of the reasons we deprecated it is that many users didn't understand that@babel/polyfill
just importedcore-js
code, effectively not giving to the project the recognition it deserved.- (NOTE:
@babel/runtime
contains all the Babel runtime helpers.) @babel/runtime-corejs2
is@babel/runtime
+ "proxy files" tocore-js
. Imports to this package are injected by@babel/plugin-transform-runtime
, similarly to how@babel/preset-env
injects imports tocore-js
.@babel/runtime-corejs3
is the same, but depending oncore-js-pure
3 (which is mostlycore-js
but without attaching polyfills to the global scope).
With the polyfill providers proposed in this RFC, we will just generate imports to core-js-pure
when using @babel/plugin-transform-runtime
rather than using the @babel/runtime-corejs3
"proxy".
Related issues
- Element instanceof EventTarget #9289
- https://github.com/babel/babel/issues/9666
- Setting broserslist to "ie 9" lacks "MutationObserver" object #9842
- @babel/preset-env doesn't include fetch polyfill #9160
- IE: Object doesn't support property or method 'remove' #8406
- response.getHeaders babel polyfill #7052
- Fetch API feature request #2113
- Allow whitelist and blacklist options for runtime transformer definitions (T6904) #3934
- plugin-transform-runtime doesn't honor preset-env targets when corejs option is set #9363
- Move transform-runtime functionality into this preset #6629
- ...