Tags: web-dev concept

Browser Compatibility

Date: 2026-08-17


Deciding what you can use, from evidence rather than habit. The two skills are reading support data against your own traffic rather than global averages, and detecting features rather than browsers — because the browser-sniffing approach has been wrong for twenty years and still gets written.


Browser compatibility is whether a feature works as intended across the browsers and versions your users actually run.

Feature detection, not browser detection

// ✗ browser detection. wrong the moment a string changes, and the
//   user agent is deliberately being reduced anyway
if (navigator.userAgent.includes('Safari')) { useFallback(); }
 
// ✓ detect the thing you actually need
if ('IntersectionObserver' in window) { observe(); } else { fallback(); }
 
if (CSS.supports('container-type: inline-size')) { … }
/* the CSS version — no JavaScript needed */
@supports (view-transition-name: none) { … }
 
/* and the implicit one: an unsupported property is simply ignored */
.card { display: flex; display: grid; }   /* grid wins where supported */

User agent strings are being frozen and reduced as an anti-fingerprinting measure, so parsing them is a decaying strategy on top of being a wrong one — Fingerprinting, Browser Privacy Restrictions.

Reading support data properly

Global usage figures are not your traffic, and the gap decides real questions.

a feature at 91% global support

your traffic                        the decision

  UK retail, 62% mobile             check YOUR Safari and Android
  Safari 34%, Chrome 41%,           versions specifically — if the
  Firefox 4%, other 21%             unsupported 9% is concentrated in
                                    an old Safari you have a lot of,
                                    that's 6% of YOUR revenue

  internal admin tool,              91% is irrelevant. you support
  Chrome on managed laptops         one browser and it's current

Get the real distribution from your own analytics, segmented by revenue rather than sessions. Old browsers skew towards older devices, which skew towards particular customer segments — and on some retailers those segments spend more, not less — Segmentation (analysis).

Baseline is the useful shared vocabulary: a feature is “newly available” once it’s in all major engines, and “widely available” after roughly 30 months of that. It’s a much better basis for a team convention than “is it on caniuse”.

Setting a support target

Write it down once, as a policy, rather than arguing per feature:

TIERED SUPPORT

  FULL         current and previous version of each major browser
               → every feature, full experience

  FUNCTIONAL   anything back to [a stated date or version]
               → can browse, add to basket, check out
               → may lack animations, container queries, niceties

  UNSUPPORTED  below that
               → served a working baseline, no guarantees
               → NOT blocked. never block

Never block an unsupported browser. A “please upgrade” page on a corporate machine the user can’t upgrade is a lost sale for no benefit — and it’s frequently shown to a modern browser that was misidentified.

Tie the policy to a number. “We support browsers covering 98% of our revenue, reviewed each quarter” is a decision that can be revisited with evidence, rather than a preference.

Polyfills, transpilation and their costs

POLYFILL       adds a missing API at runtime
               costs bytes for EVERYONE unless conditionally loaded

TRANSPILE      rewrites modern syntax to older syntax at build time
               costs bytes and often produces slower code
               — Transpilation

PONYFILL       a standalone implementation you import explicitly,
               rather than patching globals. safer, no global side effects

A fourth term belongs beside these and isn’t in the table because it answers a different question: a shim wraps an API that is present but doesn’t behave as you need. Only polyfills are ever deletable — Shims and Polyfills.

The browserslist target is the single highest-value line to check. A stale one silently transpiles and polyfills for browsers nobody uses, and it’s routinely worth 40–60 KB of a bundle — Bundle Analysis.

Differential serving — a modern bundle for modern browsers, a legacy one for the rest — removes the cost for the majority:

<script type="module" src="/app.modern.js"></script>
<script nomodule src="/app.legacy.js"></script>

Modern browsers ignore nomodule; old ones don’t understand type="module" and skip it. Two bundles, each served to the right audience, no user agent parsing.

Testing it

  • Real devices for the important ones. Emulators get layout roughly right and input, scrolling, and Safari’s quirks wrong. One cheap Android and one older iPhone cover most of what breaks
  • A device lab service for the long tail, wired into CI for the critical journeys — End-to-End Testing
  • Test the functional tier deliberately, not just the full one. Nobody checks whether checkout works on the browsers they claim to support functionally, which is exactly where the revenue risk sits
  • Watch Error Tracking segmented by browser. A spike concentrated in one version is the earliest signal that a feature you shipped isn’t as supported as you thought

The pragmatic position

Use modern features and enhance progressively. Waiting for universal support means never using anything, and the cost of a fallback is usually one @supports block or one feature check.

✓  use it, with a working baseline underneath   — Progressive Enhancement
✓  use it where the fallback is "slightly less nice"
✗  use it where the fallback is "checkout doesn't work"

The line is the commercial path. Anything on browse → basket → checkout gets a tested fallback; anything else can degrade to nothing much.

Where it interacts