Polyfills and Baseline Targets
Ship modern JS safely: polyfill vs transpile, core-js, browserslist, Baseline, and how to avoid shipping dead code to everyone.
- 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/tsctarget logs- Bundle visualizer: is
core-jshalf 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
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.