Choosing a core-js 3.x polyfill strategy under browserslist without blowing bundle size
A decision guide for core-js 3.x polyfills under browserslist: compare global stable, selective ES imports, core-js-pure and bundler automation, with trade-offs and a concrete per-feature import pattern.
22 Aug 2025, 21:22 UTC

The decision
You need ES2015+ features in an app that still runs on browsers from 2018 onward, optionally IE11. The decision is which core-js 3.x import pattern to use so you get coverage without shipping unused polyfills, without prototype pollution conflicts, and with reproducible builds.
Useful takeaway: for apps, avoid a global import 'core-js/stable' in production. Prefer a browserslist-driven, per-feature import set, and keep regenerator-runtime separate. For libraries, prefer core-js-pure.
Constraints that shape the choice
- Bundle size budget. core-js/stable pulls the entire stable set. Unused features cost bytes after gzip.
- No duplicate globals. Mixing explicit core-js imports with bundler-provided polyfills creates duplicate symbol definitions and runtime cost.
- Build reproducibility. Imports should be explicit and version pinned. core-js 3.x paths differ from core-js 2.x.
- Maintainable imports. As code evolves, the set of needed features should be discoverable, not a manual checklist that drifts.
browserslist is a config standard used by Babel and PostCSS to define target environments. Tree shaking is the bundler ability to drop unused exports. Prototype pollution refers to mutating built-in prototypes like Array.prototype, which core-js does by default.
Options compared
| Strategy | Import shape | Size impact | Maintenance | When it fits |
|---|---|---|---|---|
| Global stable | import 'core-js/stable'; import 'regenerator-runtime/runtime' | Largest. Full global coverage. | Low. One import. | Prototypes, quick start, internal tools. |
| Selective ES imports | import 'core-js/es/array/flat-map' per feature | Smallest controllable. Tree shakable. | Medium. Must track usage. | Production apps with size budget. |
| core-js-pure | import flatMap from 'core-js-pure/es/array/flat-map' | Similar to selective, no globals. | Higher. Manual wiring. | Libraries, tests, avoiding global mutation. |
| Bundler polyfills | Babel useBuiltIns: 'usage' | Opaque. Risk of duplication. | Low. Automated. | When you trust toolchain and audit output. |
Trade-offs
Global stable
Fastest to implement and safest for coverage. You get all stable features polyfilled globally. Cost is unused polyfills and larger bundles. Accidental use in production can exceed size budgets.
Selective imports
Optimizes size and load time by importing only features you use. Requires discipline to keep imports aligned with code. Risk of missing a feature if a new ES method is added without a matching import.
Pure mode
core-js-pure provides the same features without mutating globals. Useful for libraries to avoid conflicts. You must assign results to globals yourself if legacy support is needed, otherwise runtime errors occur in old environments.
Bundler automation
Reduces boilerplate but makes size impact opaque. Mixing with explicit core-js imports can cause duplicate globals and increased runtime cost.
Concrete implementation pattern for an app
Assume core-js 3.x. Version assumptions matter: import paths are core-js/es/* and core-js/stable/* in v3.
Configure targets. In package.json at project root:
{
"browserslist": [
">0.5%",
"last 2 versions",
"not dead",
"iOS >= 12"
]
}Install with user permissions in project root:
npm install core-js@^3 regenerator-runtimeExpected check: package.json and node_modules/core-js present. Risk: version mismatch with existing Babel config.
Entry point imports. Create or edit src/entry.js and import only features you actually use:
import 'core-js/stable/string/pad-start';
import 'core-js/es/array/flat-map';
import 'core-js/es/promise';
import 'regenerator-runtime/runtime';Do not import core-js/stable in production builds. Align imports to code usage. If you use async generators, keep regenerator-runtime. If not, omit it.
Validation
Build the app with the chosen import set and measure final bundle size and gzip versus the global import baseline using a bundle analyzer. Run the build in CI to keep results reproducible.
Open the app in the oldest target browser and verify targeted features such as Array.flatMap and String.padStart work without errors. Feature detection checks are preferable to user-agent sniffing.
Search the production bundle for duplicate polyfill code and confirm only explicitly imported core-js modules are present. When using core-js-pure, run unit tests that check for global prototype mutation to confirm isolation.
Limitations
Selective imports require ongoing upkeep as new ES features are adopted. core-js-pure does not polyfill globals automatically; forgetting to assign results causes runtime errors in legacy environments. Mixing core-js with Babel polyfills can cause duplicate globals.
Practical way to check result: compare bundle size and gzip before and after switching strategies, and run a manual smoke test in the lowest supported browser for the imported features.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.