Optimizing Polyfills with Babel’s useBuiltIns: 'usage'
Learn how Babel’s useBuiltIns: 'usage' option adds only the polyfills your code actually uses, reducing bundle size while maintaining compatibility.
11 Dec 2025, 01:48 UTC

Problem: Unnecessary polyfills bloat your bundle
When you target older browsers, it’s tempting to add a preset that injects every possible core-js polyfill. The result is a larger JavaScript bundle, slower load times, and wasted bandwidth for features your code never uses.
Thesis: Use @babel/preset-env with useBuiltIns: 'usage' to include only the polyfills actually referenced in your source files.
How the option works
During the Babel transform, useBuiltIns: 'usage' walks the abstract syntax tree (AST) of each file. If it encounters a reference to a built‑in such as Promise, Array.prototype.includes, or Map, it automatically inserts an import statement for the corresponding core-js module (e.g., import 'core-js/es/promise'). Built‑ins that are not touched are omitted, so the final bundle contains only the polyfills you need.
This behavior works with both core-js@2 and core-js@3. You must declare the version you want in the preset options (corejs: 3) so Babel knows which module paths to generate. Mismatched versions can lead to missing or duplicate imports.
Worked example
Suppose you have a small utility file src/util.js that uses Promise and Array.prototype.includes:
// src/util.js
export function fetchData() {
return fetch('/api/data')
.then(r => r.json())
.then(arr => arr.filter(x => x.active));
}
Your Babel configuration (babel.config.json) looks like this:
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
Running Babel (e.g., npx babel src --out-dir lib) will produce something similar to the following in lib/util.js (illustrative only):
"use strict";
require("core-js/es/promise");
require("core-js/es/array/includes");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.fetchData = void 0;
function fetchData() {
return fetch('/api/data')
.then(r => r.json())
.then(arr => arr.filter(x => x.active));
}
exports.fetchData = fetchData;
Notice the two require statements that correspond exactly to the built‑ins used in the source. If you later remove the includes call and rebuild, the second require line disappears.
Trade‑offs and limitations
- Static analysis limits: The option can only detect usages that are visible statically. Dynamic patterns like
eval('Promise')or property access via a variable (const p = Promise; new p()) may be missed, potentially causing runtime errors in older browsers. Mitigate this by writing explicit imports (import 'core-js/es/promise') or by adding a fallback entry‑point import. - Build‑time overhead: Traversing every file’s AST adds a small cost to each build. In very large monorepos or when using Babel’s watch mode, you might notice a slight increase in incremental rebuild time.
- Syntax still needs transpiling:
useBuiltIns: 'usage'only handles polyfills for built‑ins. New syntax features (e.g., optional chaining, private class fields) still require the appropriate@babel/plugin-proposal-*plugins or a suitabletargetssetting.
Actionable steps
- Install the required packages if you haven’t already:
npm install --save-dev @babel/core @babel/preset-env core-js@3 - Add or update your Babel config as shown above, ensuring
corejsmatches the installed core-js version. - Run a build and inspect the output for a few files you know use built‑ins. Look for the generated
require/importstatements. - Verify bundle size improvement with a tool like
webpack-bundle-analyzerorrollup-plugin-visualizer. Compare the core-js chunk size before and after enabling the option. - Test the built code in a target older browser (e.g., IE11 via BrowserStack) to confirm that the used built‑ins work as expected.
- If you suspect missing polyfills from dynamic usage, add a manual import at your app’s entry point:
This hybrid approach catches any static misses while still benefiting from selective imports elsewhere.import 'core-js/stable'; import 'regenerator-runtime/runtime';
Closing
Choosing useBuiltIns: 'usage' is a practical engineering decision that trims unnecessary polyfills, improves load performance, and keeps your build predictable. Pair it with careful testing of dynamic code paths and a sensible core-js version, and you’ll get the smallest possible bundle without sacrificing compatibility.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.