Right-Sizing CoreJS Polyfills: Entry Points, Babel Integration, and Bundle Verification
Stop shipping all of core-js/stable. Choose the right CoreJS entry point, wire up Babel's useBuiltIns: 'usage', and verify the bundle actually shrank.
27 Jan 2026, 21:16 UTC

Shipping the whole of core-js/stable to every visitor costs roughly 100–200 KB minified before your own code runs. The fix is not a different polyfill library — it is choosing the right CoreJS entry point and letting Babel (or explicit imports) pull in only what your target browsers actually lack. This guide walks through that decision, the configuration, and how to verify the result.
What you are deciding
CoreJS 3.x splits polyfills into granular modules. There are three practical ways to consume them:
- Full entry points —
import 'core-js/stable'(stable ECMAScript features) orimport 'core-js/es'. Simple, but imports everything regardless of need and defeats tree-shaking. - Babel-driven injection —
@babel/preset-envwithuseBuiltIns: 'usage'scans your source and inserts only the per-feature imports your targets require. - Manual per-feature imports — e.g.
import 'core-js/features/array/flat-map'. Maximum control, useful when you do not run Babel or want to polyfill one specific gap.
For most applications, Babel-driven injection is the right default. Manual imports suit libraries-in-progress, small scripts, or non-Babel toolchains.
Prerequisites
- CoreJS 3.x installed (
npm ls core-jsto confirm the resolved version). CoreJS 4 exists but is not a drop-in upgrade — some proposal paths moved — so this guide assumes 3.x unless noted. @babel/preset-envin your build, or a bundler (webpack 5+, Rollup, esbuild) if you plan manual imports.- A
browserslistconfiguration describing your real audience. Runnpx browserslistto see what it currently resolves to — the polyfill set is derived from this, so a stale config silently changes your bundle.
Configure Babel for usage-based injection
In babel.config.json (project root, applied by your build tool):
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": { "version": 3, "proposals": false }
}]
]
}Key behaviors to understand:
useBuiltIns: 'usage'adds imports at the top of each file that uses a missing feature. No central import is needed — do not also importcore-js/stablein your entry file, or you get both.useBuiltIns: 'entry'is the alternative: you write oneimport 'core-js/stable'at your app entry and Babel rewrites it to the full set your targets need. This still over-includes compared tousagebecause it polyfills features you never call.proposals: falsekeeps you on stable ECMAScript features. Enable it only if you deliberately rely on proposal-stage APIs.
If you maintain a library, do none of this. Do not bundle CoreJS and do not inject global polyfills into consumers' apps. Either declare the features you need in documentation, or use @babel/plugin-transform-runtime with corejs: 3, which imports non-polluting ponyfill-style helpers instead of mutating globals.
Manual per-feature imports (no Babel)
Browse node_modules/core-js/features/ to find exact paths, then import at your entry point:
// src/index.js — runs before the rest of the app
import 'core-js/features/promise/finally';
import 'core-js/features/array/flat-map';
import 'core-js/features/object/from-entries';With ES modules and a production build (NODE_ENV=production), modern bundlers tree-shake unreferenced modules. CommonJS output may not shake cleanly unless the bundler respects the package's sideEffects metadata — verify rather than assume.
What CoreJS will not fix
CoreJS covers ECMAScript language features only: Promise, Array methods, Map/Set, String methods, and so on. It does not polyfill web platform APIs. If your targets lack fetch, URL, IntersectionObserver, or WebCrypto, you need separate packages (e.g. whatwg-fetch) or a runtime service such as polyfill.io that serves a tailored bundle per user-agent. For public sites with long-tail browser traffic, a per-UA service can beat any compile-time strategy because modern browsers then download zero polyfill bytes.
Verify the result
- Resolved targets:
npx browserslist— confirm the list matches your intended support matrix. - Bundle contents: run a production build and inspect it with webpack-bundle-analyzer or Rollup's plugin-visualizer. Search for
core-jsmodules. Withusagemode you should see a handful of feature modules, not the fulles.*tree. - Server bundles: if you build an SSR or test bundle, check that Node-targeted builds do not include CoreJS at all — modern Node already implements these features. Use a separate Babel config or environment for server output.
- Runtime check: test in an actual old browser (a VM or a cloud device service). A quick smoke test: evaluate
Promise.prototype.finally,[].flatMap, andObject.fromEntriesin the console and confirm they are functions, notundefined.
Recovery and pitfalls
- Double polyfilling: if both a manual
core-js/stableimport andusagemode are active, remove the manual import. This is a config-only change — revert by restoring the import. - Global conflicts: CoreJS mutates global prototypes. In micro-frontend setups where multiple apps share a page, two CoreJS versions can clobber each other. Prefer
@babel/plugin-transform-runtimefor the embedded apps. - Upgrading to CoreJS 4: it dropped IE 11 support and moved some proposal paths. After bumping the version, rebuild and re-check every explicit
core-js/features/...path againstnode_modules/core-js/features/; broken paths fail at build time, which is the easy case — silently changed behavior is not, so re-run your old-browser smoke tests.
The takeaway: let useBuiltIns: 'usage' do the bookkeeping, keep your browserslist honest, and confirm with a bundle analyzer that the polyfill section of your output shrank to what you actually use.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.