Ship smaller bundles with Babel preset-env usage mode and core-js 3
Use @babel/preset-env with useBuiltIns: 'usage' and core-js 3 to ship only the polyfills your code actually needs for your target browsers.
01 Jul 2025, 23:24 UTC

The problem: polyfills you do not need, or polyfills you do
You add optional chaining or nullish coalescing to your source code, run the build, and discover the bundle still ships a large polyfill file for every browser on your support list. Worse, an older customer’s browser throws because Promise is undefined. The common fix—importing a universal polyfill bundle at the application entry point—often sends code your users will never execute.
The useful takeaway is that @babel/preset-env can statically scan your source and inject only the core-js 3 polyfills for features you actually use, scoped to your browserslist targets. Browserslist is the configuration format Babel reads to decide which browser versions to support. This keeps bundles smaller for modern clients without leaving older targets unsupported—provided you declare core-js explicitly and understand what static analysis cannot see.
How useBuiltIns: “usage” works with core-js 3
Babel 7’s @babel/preset-env has two main polyfill strategies. useBuiltIns: "entry" expects one import at your application entry point and includes polyfills for everything on your browserslist list. useBuiltIns: "usage" inspects each file and inserts targeted imports, such as core-js/stable/promise, only where the feature appears in your source.
core-js 3 is the actively maintained polyfill library. Babel does not ship it, and it does not assume a version. You must install it separately and set corejs explicitly in your preset options, for example { version: 3, proposals: false }. Setting proposals: false excludes stage 3 proposal polyfills, keeping the output closer to standardized features.
Configure Babel and browserslist
Example babel.config.json:
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": { "version": 3, "proposals": false }
}]
]
}
Example .browserslistrc in the project root:
last 2 Chrome versions
last 2 Firefox versions
last 2 Safari versions
ie 11
Without an explicit corejs version, Babel may not apply the intended polyfills. Without an accurate browserslist, Babel can over-polyfill for modern users or under-polyfill for older targets.
Worked example: install, build, and inspect
- Run in the project root:
npm install core-js@3. core-js is a runtime dependency because Babel inserts import statements into your bundled code. - Confirm the resolved configuration:
npx babel --print-configor openbabel.config.json. Look foruseBuiltIns: "usage"andcorejs.version: 3. - Build the project, for example
npm run build. - Search the build output for targeted imports, such as
core-js/stable/promise, and confirm they appear only where your source uses the feature. - Optionally switch to
useBuiltIns: "entry"in a temporary branch, rebuild, and compare bundle size. Entry mode typically includes more polyfills because it covers every feature on your browserslist list.
Risk: if core-js is missing or not installed, code that relies on injected polyfills can throw at runtime in older browsers. An outdated browserslist can cause over-polyfilling or under-polyfilling.
What static analysis cannot catch
Usage mode detects feature references in statically analyzable code. It can miss polyfills for features used inside strings, eval, dynamically generated code, or runtime feature detection. If your application loads polyfills conditionally at runtime, Babel may omit an import your code path still needs. In those cases, import the specific core-js module manually or use entry mode for that feature.
Closing checklist
- Install core-js@3 as a dependency, not only a dev dependency.
- Set
corejsexplicitly; do not rely on Babel defaults. - Review browserslist targets before each release.
- Verify build output contains the expected core-js imports.
- Test the bundle in the oldest browser version you claim to support.
If the build output lacks an import for a feature your source uses, check whether that feature appears in dynamically constructed code. If it does, add a manual core-js import rather than changing the global preset strategy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.