Fine‑Tuning Babel: Using @babel/preset‑env with core‑js 'usage' to Ship Modern JS to Legacy Browsers
Learn how to configure Babel’s preset‑env with browserslist targets and core‑js ‘usage’ to inject only the polyfills you need, shrink bundle size, and keep legacy browser support.
24 Sept 2026, 13:49 UTC

The Problem: Modern Syntax, Old Engines
When you write code with async/await, Array.prototype.flat, or Promise, the JavaScript engines in browsers like IE 11 or early Edge simply don’t understand it. A naive build that transpiles everything to ES5 and bundles a full core-js polyfill ends up with a 1 MB+ payload that slows down startup and inflates cache size.
The Solution: Targeted Transpilation + On‑Demand Polyfills
Babel’s @babel/preset‑env reads a browserslist query (from package.json or .browserslistrc) and only transpiles syntax that the selected browsers can’t handle. By pairing it with core-js and useBuiltIns: 'usage', Babel will scan your source, detect the APIs you actually call, and inject the minimal set of polyfills. The result is a leaner bundle that still runs everywhere you promise support for.
Key Configuration Pieces
// babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: { browsers: "> 0.5%, not dead" },
useBuiltIns: "usage",
corejs: 3
}
]
]
};
In package.json you can keep a simple browserslist field:
"browserslist": ["> 0.5%", "not dead"]
Running npx browserslist will list the engines that will be considered during the build, giving you confidence that the targets match your marketing or compliance promises.
Concrete Example
Suppose your module contains:
async function loadData() {
const response = await fetch("/api/data");
const data = await response.json();
return data.items.flat();
}
With the configuration above, Babel will:
- Transpile
async/awaitto generator +regenerator-runtimeonly if the target browsers lack native support. - Inject
import "core-js/modules/es.promise.js";andimport "core-js/modules/es.array.flat.js";becausePromiseandArray.prototype.flatare used. - Leave
fetchuntouched – it’s not a polyfillable API incore-js, so you’ll need a separate polyfill or a runtime check.
Resulting bundle snippet (simplified):
import "core-js/modules/es.promise.js";
import "core-js/modules/es.array.flat.js";
import regeneratorRuntime from "regenerator-runtime/runtime";
function loadData() {
return regeneratorRuntime.wrap(function* () {
const response = yield fetch("/api/data");
const data = yield response.json();
return data.items.flat();
});
}
Trade‑offs & Limitations
| Aspect | 'usage' | 'entry' |
|---|---|---|
| Polyfill granularity | Only APIs you call are polyfilled | All APIs covered by targets are polyfilled, even if unused |
| Analysis complexity | Static analysis may miss dynamic access (e.g., window["Promise"]) | Never misses dynamic usage but may over‑polyfill |
| Bundle size | Smaller, but still includes regenerator-runtime if needed | Larger because of full polyfill set |
Other caveats:
- Node_modules are not polyfilled by
preset‑envby default. If a library you import usesPromisein IE 11, you must either patch it or ship your own polyfill. - Mixing
preset‑envwith@babel/plugin-transform-runtimecan duplicate helpers or polyfills. Pick one strategy per project. - core‑js major versions differ;
corejs: 3is the current stable path. Usingcorejs: 2will trigger legacy behavior. - Polyfills for
fetch,IntersectionObserver, etc., are not part ofcore-jsand require separate ponyfills or services.
Actionable Checklist
- Define a
browserslistthat reflects your support matrix. - Add
@babel/preset‑envwithuseBuiltIns: 'usage'andcorejs: 3tobabel.config.js. - Run
npx browserslistto verify targets. - Build and inspect the output: look for
core-js/modules/...jsimports. - Compare bundle sizes with
useBuiltIns: falseto quantify savings. - Test the final bundle in a real legacy browser or an emulator to catch any missed polyfills.
- If you ship a library, consider
@babel/plugin-transform-runtimeand avoid global polyfills.
By following this pattern, you ship the smallest possible bundle that still runs on the browsers you promise, while keeping your build process deterministic and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.