ESC

Type to search the knowledge base.

Polyfills and Baseline Targets

Ship modern JS safely: polyfill vs transpile, core-js, browserslist, Baseline, and how to avoid shipping dead code to everyone.

intermediate3 min read
  • javascript
  • polyfills-and

Your laptop runs Chrome 120+. A share of users still hit older Safari or embedded WebViews. Polyfills add missing APIs at runtime. Transpilation rewrites syntax (?., classes) to older forms. They solve different gaps — confuse them and you either break old clients or bloat modern ones.

Polyfill vs transpile

Gap Tool Example
Syntax Babel / SWC / TypeScript optional chaining, private fields
Built-ins / APIs polyfill Promise, Array.prototype.at, fetch
Both often both in legacy targets async functions need transform + regenerator/runtime in old setups
// Syntax — cannot polyfill; must parse
const x = obj?.y;

// API — can polyfill
if (!Array.prototype.at) {
  Array.prototype.at = function (i) {
    i = Math.trunc(i) || 0;
    if (i < 0) i += this.length;
    return this[i];
  };
}

Monkey-patching natives is powerful and risky: order of polyfills, non-compliant shims, and test environments that “pass” only because of a polyfill.

Baseline and browserslist

Baseline (and Can I Use / MDN BCD) describe what’s widely available. Your browserslist is the contract your build actually targets:

# package.json
"browserslist": [
  "defaults",
  "not dead",
  "iOS >= 15"
]

Babel/@babel/preset-env and core-js use that list to decide which transforms and polyfills to include.

core-js and useBuiltIns

// babel.config — conceptual
presets: [
  ['@babel/preset-env', {
    useBuiltIns: 'usage', // inject per-file imports for used features
    corejs: 3,
    bugfixes: true,
  }],
]
Mode Behavior
usage Import only polyfills your source touches
entry Giant import in entry based on browserslist
false No polyfills from preset-env

usage is usually the right default; still audit the bundle — dynamic patterns can miss detection, and polyfilling Array.prototype methods has size costs.

Don’t polyfill what you don’t need

// You only need Promise.allSettled in one admin route
// → dynamic import a local helper, or gate the feature,
// not a global allSettled polyfill for the marketing page

Feature-detect and progressive-enhance when the feature is optional:

if ('IntersectionObserver' in window) {
  // lazy images with IO
} else {
  // load all images / simple fallback
}

Runtime polyfill services

CDN “polyfill.io”-style services inject based on UA. Convenient historically; now a supply-chain and correctness risk if you don’t control the host. Prefer build-time inclusion you pin and review.

Testing the real target

Unit tests on latest Node don’t prove Safari 14. Use:

  • BrowserStack / Sauce / Playwright projects with older engines
  • esbuild/tsc target logs
  • Bundle visualizer: is core-js half your vendor chunk?

Interview answer (out loud)

“Syntax needs transpilation; missing APIs need polyfills. I set a browserslist aligned with product support and Baseline data, let preset-env + core-js inject usage-based polyfills, and feature-detect optional APIs instead of polyfilling the world. I verify with real target browsers and watch bundle size.”

Further reading

Related guides