Choosing core-js polyfills: full bundle or targeted imports
Decide between full core-js, modular feature imports, and Babel usage-based polyfills based on bundle size and target browsers.
16 Dec 2025, 05:29 UTC

The decision: ship every polyfill or only the ones you need
If your site must support older browsers, a missing polyfill breaks features such as Array.prototype.flat or Promise. If you import every available polyfill, visitors download code their browsers already provide. A polyfill is code that adds a modern JavaScript feature to an older environment. The practical takeaway is to match the import strategy to what you know about your targets: use Babel usage-based injection for applications you build, explicit modular imports for small known gaps, and reserve the full core-js stable entry point for cases where you cannot analyze usage or need immediate broad coverage.
Constraints to check before choosing
- Target environments: browser versions, Node versions, or both.
- Build tool: Babel with webpack, Rollup, Vite, or another bundler.
- Package type: applications can own global polyfills; libraries generally should not modify native prototypes because a prototype is the shared object that all instances of a type, such as Array, inherit from.
- Dependency tree: confirm whether another dependency already brings in core-js, because mixing major versions can duplicate polyfills.
Supported options compared
| Option | What it imports | Best for | Main risk |
|---|---|---|---|
| core-js/stable | All stable ECMAScript features | Rapid legacy support with minimal configuration | Larger bundle and global prototype changes |
| core-js/features/array/flat and similar paths | One specific feature | Small, known gaps in a controlled codebase | Manual maintenance; easy to miss a needed feature |
| @babel/preset-env with useBuiltIns: usage | Polyfills Babel detects in your source files | Applications where build-time analysis is possible | Only scans files in the dependency graph; dynamically loaded code may be missed |
| @babel/preset-env with useBuiltIns: entry | One manual import that Babel replaces with needed polyfills | Explicit entry-point control | Must maintain the import; still applies polyfills globally |
Trade-offs
Global pollution is the most important engineering risk. When core-js adds a method to Array.prototype, every script in the same realm sees that method. If two parts of an application load different core-js versions, they can disagree about feature behavior. Bundle bloat is the second risk: core-js/stable ships everything even when targets already support most features natively. Version mismatch is the third: installing core-js 2.x and 3.x in the same tree can produce duplicate polyfill code and unexpected interactions.
Concrete implementation: Babel usage-based polyfills
This strategy suits applications built with Babel. Run the install command in the project root. No special permissions are required.
# Run in the project root. No special permissions are required. npm install --save-dev @babel/core @babel/preset-env core-js
Add a Babel configuration. Use the major core-js version installed in your project; the example shows 3 because that is the current major line, but adjust it to match your lockfile.
{
"presets": [
[
"@babel/preset-env",
{
"targets": { "browsers": ["> 0.5%", "last 2 versions", "Firefox ESR", "not dead"] },
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
With useBuiltIns: usage, you do not import core-js in your entry file. Babel inspects your source and injects only the polyfills required for the listed targets. Run your normal build command after saving the config.
Concrete implementation: manual modular imports
Use this when you know exactly which features are missing and want no Babel magic:
import 'core-js/features/array/flat'; import 'core-js/features/promise';
This is explicit and easy to audit, but you must add a new import whenever your code starts using another unsupported feature.
How to verify the result
- Build the project.
- Inspect the generated bundle. Search for feature-specific code such as Array.prototype.flat or Promise polyfill helpers. Presence confirms Babel injected something; absence may mean your targets already support the feature natively.
- Check bundle size before and after the change using your bundler’s stats output or a bundle analysis tool. Do not assume a specific reduction; measure it.
- Test in a target browser or device farm. Open the browser console and run typeof Array.prototype.flat === 'function'. In a modern browser this will be true because the feature is native, so the meaningful check is bundle content plus target-browser behavior, not the console alone.
Limitations
- Usage-based injection only sees files in the build graph. Code loaded at runtime or generated on the server may need manual imports.
- Prototype pollution remains regardless of import style. If you publish a library, avoid global polyfills and ship source that targets modern runtimes instead.
- Experimental proposals are opt-in and less stable. Do not enable proposal polyfills unless you have reviewed their specification status.
Rollback
Because installing dependencies and editing Babel configuration changes project state, rollback by reverting the configuration file and reinstalling the previous dependency tree:
# Example only; adapt to your version-control setup. git checkout -- babel.config.json package-lock.json npm ci
If you are not using Git, restore the previous configuration from your backup and run your package manager’s install command to regenerate node_modules from the earlier lockfile.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.