Right-size Babel polyfills with preset-env browser targets
Configure @babel/preset-env with explicit browserslist targets and useBuiltIns: usage so your bundle only ships the core-js polyfills your code and users actually need.
05 Nov 2026, 16:41 UTC

The problem: polyfills your users do not need
You inherit a frontend application and run the production build. The bundle is several hundred kilobytes larger than expected. Grepping the output reveals core-js polyfills for Object.fromEntries, Promise.allSettled, and String.prototype.matchAll, even though your analytics show almost all visitors use Chrome 120+ or Safari 17+, which support those features natively.
The likely cause is @babel/preset-env running with broad or missing browser targets, often paired with useBuiltIns: 'entry'. In that mode, Babel expects you to import the full core-js standard library, and the build ships every polyfill in that import regardless of whether your code uses it.
Takeaway: declare explicit browser targets, switch to useBuiltIns: 'usage', and pin core-js version 3. Babel then scans your source, emits only the polyfills your code actually calls, and only for browsers you say you support.
How preset-env decides what to emit
@babel/preset-env is a Babel preset that combines syntax transformations and polyfill selection based on browser targets. Targets are usually declared in a browserslist configuration, such as .browserslistrc or a browserslist key in package.json. Browserslist is a shared configuration format understood by Babel, Autoprefixer, and other frontend tools.
The critical preset options are:
useBuiltIns: 'usage'— Babel analyzes each file and injects individualcore-jsimports only for features present in that file and missing from your targets.useBuiltIns: 'entry'— You must addimport 'core-js/stable'to your entry point. Babel polyfills everything core-js offers for your targets, whether or not your code uses it.useBuiltIns: false— No automatic polyfill injection. You manage polyfills yourself.
core-js is a community-maintained library that polyfills ECMAScript standard library features. Pinning its major version in Babel prevents silent upgrades from changing your bundle.
Worked example: configure usage mode
Run these commands in your project root. No special permissions are required beyond normal file access.
- Check the installed Babel version:
npx babel --version
- Add explicit targets. Example
.browserslistrc:
# .browserslistrc
last 2 Chrome versions
last 2 Safari versions
last 2 Firefox versions
> 1%
- Update your Babel configuration. Example
babel.config.json:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 Chrome versions",
"last 2 Safari versions",
"last 2 Firefox versions",
"> 1%"
]
},
"corejs": {
"version": "3",
"proposed": false
},
"useBuiltIns": "usage"
}
]
]
}
- Transpile a sample file and inspect the result. Replace
src/app.jswith an actual entry file:
npx babel src/app.js --out-file dist/app.compiled.js
Open dist/app.compiled.js and search for core-js. If your source does not use Object.fromEntries and your target browsers support it natively, you should not see a corresponding polyfill import. If you see polyfills for features your code never calls, your targets are likely too broad or useBuiltIns is still set to entry.
Risk: usage works on statically analyzable code. If you load modules dynamically or rely on polyfills in third-party scripts that Babel does not process, those features may be missing at runtime. Verify by testing in your oldest supported browser after the change.
Trade-offs and limitations
| Mode | Bundle impact | When to use |
|---|---|---|
usage | Usually smallest; includes only detected features | Application code you control and can analyze statically |
entry | Larger; includes whole polyfill set for targets | Mixed codebases or dynamic loading where static analysis misses needs |
false | No automatic polyfills; full manual control | You already load a custom polyfill service or runtime |
Build time can increase slightly with usage because Babel parses every file to detect feature usage. Also, proposed: true pulls in stage proposals that may change or never standardize; keep it false for production unless you have a specific need.
Closing: verify before you ship
- Run
npx babel --versionand confirm you are on a current Babel 7 release. - Add or correct
.browserslistrcso it matches your actual audience, not a generic default. - Set
useBuiltIns: 'usage'andcorejs.version: '3'in your preset-env config. - Transpile a representative file and grep the output for unexpected
core-jsimports. - Build the full bundle, compare its size against the previous version, and smoke-test the oldest browser in your target list.
If the output still contains polyfills you do not expect, inspect your browserslist defaults first—defaults can include older browsers than your analytics suggest. Reverting is as simple as restoring the previous Babel config and rebuilding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.