How to Audit core-js Polyfill Weight in Your Bundle

This guide belongs to Polyfills & Differential Serving within JavaScript Bundle Optimization & Code Splitting. core-js is the standard polyfill library behind Babel's preset-env, and it is extremely thorough: it polyfills not only missing features but also features with bugs in specific engine versions. That thoroughness means a bundle can carry dozens of polyfill modules even when targeting reasonably modern browsers, often for edge-case spec compliance your code never depends on.

An audit answers three questions: which polyfills are in the bundle, why each was included (which browser in your matrix, which code using which feature), and whether removing it is safe. Most audits find a mix of polyfills that are genuinely needed for a recent API, polyfills that exist because the Browserslist query is outdated, and a whole-library import someone added years ago "to be safe".

core-js audit workflow Steps of a polyfill audit from listing included modules to removing unnecessary ones and verifying. core-js audit workflow List modules debug output + maps Attribute which browser needs it Classify needed / stale / blanket Remove config or import change Verify tests in oldest target

Rapid Diagnosis

  • Search the entry for blanket imports: import 'core-js', import 'core-js/stable' or import '@babel/polyfill' include everything for your targets regardless of usage.
  • Run Babel with debug output to see which polyfills each file triggers and which target browser requires them.
  • Look at the treemap for a core-js block; its size tells you whether this audit is worth an afternoon (over 10KB compressed usually is).
  • Check corejs version in Babel config. An unpinned or old minor version makes Babel's decisions less precise.

Root Cause Analysis

1. Blanket polyfill imports. useBuiltIns: 'entry' with import 'core-js/stable' expands into every polyfill required by any browser in the matrix — regardless of whether your code uses those features.

2. Bug-fix polyfills. core-js replaces native implementations known to be buggy in certain versions. If one old Safari in your matrix has a Promise or Array.prototype.sort quirk, every user gets the replacement.

3. Outdated targets. A matrix including browsers from several years ago pulls in polyfills for ES2015–ES2019 features universally supported today.

4. Proposals and stage features. core-js/proposals or shippedProposals: true add polyfills for features not yet standardised, often unnecessarily.

core-js weight by configuration (same app, brotli) Bar chart of core-js bytes in the bundle under four configurations from blanket import to usage-based with a modern matrix. core-js weight by configuration (same app, brotli) entry + core-js/stable, old matrix 46KB usage, old matrix 21KB usage, current matrix 6KB usage, current matrix, bug-fix review 3KB

Step-by-Step Resolution

1. List what Babel injects and why

javascript
// babel.config.js — temporary audit settings.
module.exports = {
  presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: '3.40', debug: true }]],
};
// Build once and read the log: each file lists "Added following core-js polyfills:"
// with the browsers that require each one.
// trade-off: debug output is extremely verbose on large apps. Redirect it to a
// file and grep for the polyfills you suspect, rather than reading it all.

Expected outcome: for each polyfill, the target browser(s) that triggered it.

2. Replace blanket imports with usage-based polyfilling

Remove import 'core-js/stable' from the entry and switch to useBuiltIns: 'usage' so polyfills are injected only where features are used.

javascript
// Before (entry file):  import 'core-js/stable'; import 'regenerator-runtime/runtime';
// After: no imports; Babel injects what each file needs.
// trade-off: usage detection is static. Features reached dynamically — e.g.
// obj[methodName]() — or used by un-transpiled node_modules are not detected.
// Add those few polyfills explicitly in the entry.

Expected outcome: polyfill weight drops to what your code demonstrably uses.

3. Remove polyfills required only by browsers you no longer support

Cross-reference the debug output with your support policy. If a polyfill is required only by browsers outside the policy, tighten Browserslist (see removing unneeded polyfills with Browserslist) rather than excluding modules one by one.

4. Exclude specific polyfills you have decided not to ship

For a handful of bug-fix polyfills whose edge cases your code does not exercise, exclude them explicitly — and document why.

javascript
['@babel/preset-env', {
  useBuiltIns: 'usage', corejs: '3.40',
  exclude: ['es.array.sort', 'es.string.replace'],   // engine quirks we do not depend on
}]
// trade-off: excluding a bug-fix polyfill means the buggy native behaviour is
// what affected users get. Only exclude after confirming your code does not
// rely on the fixed behaviour, and record the decision next to the config.

Expected outcome: remaining polyfills are each justified by a supported browser and a used feature.

Classifying each polyfill found Three categories of polyfills found in an audit with the action to take for each. Classifying each polyfill found Category Example Action Needed for a recent API es.array.find-last, structured-clone keep Needed only by unsupported browsers es.promise, es.symbol tighten Browserslist Bug-fix for an edge case you avoid es.array.sort exclude with a note Blanket import leftovers core-js/stable in entry remove import

Verification

Rebuild and compare the core-js block in the treemap. Run the full end-to-end suite in the oldest browser version your policy supports — this is the step that catches an over-aggressive exclusion. Measure TBT before and after on a throttled run; polyfill evaluation happens at startup, so removing tens of kilobytes typically trims 10–30ms from startup tasks on mid-tier devices.

Common Mistakes

  • Removing polyfills without a test in old browsers. Modern dev machines never exercise polyfills; a missing one surfaces only in production for the affected users.
  • Polyfilling in libraries and the app. A library that ships its own core-js copy duplicates modules your app also includes; check the treemap for multiple core-js paths.
  • Leaving regenerator-runtime after raising targets. If all supported browsers have native async/generators, the runtime should disappear; if it remains, something (often a dependency) still requires it.
  • Mixing corejs 2 and 3. Old dependencies built against core-js@2 pull in a second, incompatible polyfill set.

Alternatives to core-js for Specific Gaps

Sometimes a single missing API does not justify the polyfill machinery. Small, targeted polyfills (a few lines for Array.prototype.at or Object.hasOwn) can be added manually behind feature checks, at a fraction of the size of the core-js module with its shared internals. For heavier gaps, consider whether the feature is essential: code that uses structuredClone purely for convenience can fall back to a JSON round trip in the few browsers lacking it. The aim is not to avoid core-js on principle, but to make every polyfill a conscious choice rather than an inherited default.

Worked Example: Finding a Forgotten Blanket Import

An audit of a dashboard application found 41KB (brotli) of core-js even though its Browserslist query was already modern. Babel's debug output showed only six polyfills injected by usage — the rest came from a single line, import 'core-js/stable', added to the entry years earlier during an Internet Explorer support push and never removed. Deleting the line removed 35KB immediately. The remaining six polyfills were all legitimate (recent array and Intl methods missing in Safari 15), so they stayed. Total time spent: under an hour, most of it reading the debug log.

FAQ

Are polyfill services that serve per-browser bundles a good alternative?

Services that inspect the user agent and return only the polyfills that browser needs can produce very small payloads for modern browsers. They add a render-blocking third-party request (or a dependency on a self-hosted service), and their trustworthiness matters — a compromised polyfill CDN can inject code into every site using it. Self-host if you use this approach, and weigh it against simply tightening your targets.

Does TypeScript add polyfills?

No. The TypeScript compiler down-levels syntax according to its target but never adds runtime polyfills. If your TS target is low, you get down-levelled syntax helpers (__awaiter, __generator); set the TS target to match your matrix or let Babel/esbuild handle syntax.

How often should the audit be repeated?

Whenever the support policy changes and at least once a year. core-js releases add polyfills for new features and drop requirements as browsers fix bugs; updating the corejs version alone can change the output.

/html>