Definition
Instead of unconditionally handing polyfills (code that fills in functions missing on old browsers) to every visitor, you run one line of "is this feature missing?" in the browser and then load them only on the browsers that lack it. Modern browsers only detect and never receive the polyfill, so the cost is nearly zero.
Here, feature detection means testing directly whether a feature exists, like 'at' in Array.prototype — which is more robust than parsing the User-Agent string to guess the browser (UA is forged and omitted).
Why It Matters
A polyfill bundle easily runs tens of KB. Ship it to everyone and even modern-browser users who need no polyfill at all (usually the majority) download and parse that much every time. The fewer old browsers you must support, the bigger this waste.
Differential loading gives correctness to old browsers and performance to modern ones at the same time. The detection cost is a very cheap few dozen bytes of inline script, and the actual polyfill download is paid only by old browsers.
How It Works
- Compute the missing APIs from the support target and build a self-hosted polyfill bundle at build time.
- Put a small synchronous detection script in
<head>. It checks a few representative APIs. - Only if any one is missing does it synchronously load the polyfill bundle. If all are present, it receives nothing.
- The detection script must run before the app bundle — that way the polyfill is ready when the app starts.
<head>
<script>
// check only a few representative APIs — if any one is missing, treat as an old browser
if (
!("at" in Array.prototype) ||
!("replaceAll" in String.prototype) ||
typeof structuredClone !== "function" ||
!("hasOwn" in Object)
) {
// synchronous load: so the polyfill is ready before the app bundle runs
document.write('<script src="/polyfills-legacy.js"><\/script>');
}
</script>
</head>
Practical Application
- Open the network tab on a modern browser and confirm "no polyfill received."
- On an old browser (or by forcing detection), confirm "received and working."
- Serve the polyfill from the same origin, not an external CDN (to avoid supply-chain-attack history and availability risk).
Trade-offs
| Option | Modern-browser cost | Ops burden | Good fit |
|---|---|---|---|
| Statically bundled in the entry | +tens of KB always | None | Old browsers are the majority, or simplicity wins |
| Build-time bundle + feature-detect load | ≈ 0 (detect ~tens B) | None | Optimal when the target is an "old vs new" split |
| UA-based server-side dynamic generation | ≈ 0 | Server required | When the browser spectrum is very wide |
When Not to Use
- When the support target is effectively modern-only and no polyfill is needed — even the detection script is dead weight.
- Placing the detection script after the app bundle, so the app starts without the polyfill and blows up early.
Common Mistakes
- Detecting via the UA string and misjudging forged or brand-new browsers.
- Fetching the polyfill from an external CDN and taking on supply-chain / availability risk.
- Setting the detection script to async/defer so its execution order slips.
Related Concepts
- syntax-transpilation-vs-runtime-polyfill — What you should polyfill (runtime APIs, not syntax)
- readonly-viewer-bundle-splitting — Chunk-splitting mindset: load code only when needed