Ship fewer core-js polyfills by matching imports to your browser targets
Stop shipping every core-js polyfill to every browser. Use core-js-compat with browserslist to import only the ECMAScript polyfills your supported targets actually need.
21 Aug 2025, 07:28 UTC

The problem: one polyfill bundle for every browser
Many frontend projects start by importing core-js/stable once in their entry file. This entry point polyfills a broad set of ECMAScript features and web standards for all supported browsers. If your browserslist targets modern Chrome, Firefox, Safari, and Edge, you are still shipping code meant for older engines. The result is a larger JavaScript bundle that every visitor downloads, parses, and compiles, even when the browser already has native Promise, Array.prototype.flat, or Object.fromEntries.
The useful takeaway: you do not have to choose between shipping everything and shipping nothing. core-js@3 splits polyfills into feature-specific ES modules, and core-js-compat can help derive which ones your browserslist targets require. This post shows how to replace a blanket stable import with targeted imports, and where the trade-offs lie.
What core-js/stable includes
core-js/stable is a convenience entry point. It pulls in polyfills for built-ins, collections, typed arrays, reflection, and several web standards. Because it is a single side-effect import, bundlers often cannot tree-shake individual pieces out of it. Tree-shaking means a bundler removes code that is never imported or used by your application. If your application uses only Promise but your target browsers already support it natively, the import still adds bytes to the bundle.
Version assumption: this guidance applies to core-js@3.x. core-js v2 is no longer maintained and lacks newer spec coverage. Check your installed version before changing imports:
# Run in your project root. No special permissions required.
npm list core-js
Look for core-js@3.x.x. If you see core-js@2.x.x, upgrade first; the import paths below are for v3.
Feature-specific imports and tree-shaking
core-js@3 exports individual modules such as core-js/es/promise, core-js/es/array/flat, and core-js/es/object/from-entries. These are ES-module entry points, which means modern bundlers can include only the pieces your code actually imports and your targets actually lack.
This does not happen automatically. You still have to decide which modules to import. Importing every module individually defeats the purpose, and missing a polyfill your code needs will cause runtime failures in older browsers.
Worked example: generate a targeted polyfill list
Start with a browserslist configuration in package.json:
{
"browserslist": [
"last 2 versions",
"not dead",
">= 0.5%"
]
}
Run npx browserslist in your project root to see which browsers that resolves to. No special permissions are required. The command reads your local config and prints the target list.
The core-js-compat package is designed to generate a polyfill list from browserslist targets. Because its API can change between minor releases, inspect the README in your installed node_modules/core-js-compat before relying on a specific call signature. A typical approach is a small Node script:
const compat = require('core-js-compat');
const polyfills = compat({
targets: '> 0.5%, last 2 versions, Firefox ESR, not dead'
});
console.log(polyfills.join('\n'));
Expected check: the output should be a much shorter list than the full core-js/stable surface. If the list is empty, your targets likely do not require any core-js polyfills.
Then create a single polyfill entry point, for example src/polyfills.js, importing only the modules your code uses and your targets need:
import 'core-js/es/promise';
import 'core-js/es/array/flat';
import 'core-js/es/object/from-entries';
Import src/polyfills.js once in your application entry point, before your application code runs. Rebuild and inspect the bundle with source-map-explorer or your bundler's stats output. You should see fewer core-js modules than when importing core-js/stable. Do not treat this as a measured result; verify it in your own build.
Trade-offs and limitations
- Maintenance burden: feature-specific imports require you to track which ECMAScript features your source code uses. If you add
Array.prototype.atto your source but forget the import, older browsers will throw at runtime. - Proposal risk:
core-js/proposalsimplements stage specifications that can change before they become standards. Avoid proposal imports in stable browser builds unless you can patch quickly. - Duplicate versions: bundling two different core-js@3 minor versions in one application can cause global polyfill conflicts. Pin
core-jsinpackage.jsonand inspect your lockfile for duplicates.
Practical verification
- Confirm
npm list core-jsreports v3.x. - Run
npx browserslistand confirm the targets match your support policy. - Build with your targeted polyfill entry and open the bundle in
source-map-explorer. Compare core-js module sizes against a build that importscore-js/stable. - Load the built application in the oldest browser you claim to support, or use browser emulation, and exercise code paths that depend on polyfilled features.
If a feature fails in an older target, add the corresponding core-js/es/* import to src/polyfills.js, rebuild, and re-check. Because this change only alters source imports and build configuration, rollback is as simple as reverting the import file and redeploying the previous bundle.
Closing
core-js@3 makes it possible to ship polyfills conditionally by feature instead of by default. Pair your browserslist targets with core-js-compat, import only what you need, and verify in the bundle output. Your modern visitors get less JavaScript, and your older targets still get the features they require.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.