MonkeyLoader All articles
Infrastructure

Edge Cases: Serving IE Mode a Legacy Bundle Without Punishing Everyone Else

MonkeyLoader
Edge Cases: Serving IE Mode a Legacy Bundle Without Punishing Everyone Else

Photo: User:Ggia, CC BY-SA 3.0, via Wikimedia Commons

Most weeks someone drops a screenshot in the team channel: a council page rendering as a white rectangle in Edge, with a spinner that gave up sometime on Tuesday. The browser in question almost never turns out to be Edge proper. It is IE mode — the MSHTML engine Microsoft has kept on life support so that 2009-era procurement portals and a startling number of NHS forms keep working.

The reflex is to ship a polyfill bundle to absolutely everyone. That works in the same way a wheel clamp works. There is a smaller, more surgical option: split the build, hand the old thing only to the clients that need it, and log enough to know when you can turn it off.

IE mode is not a browser you can reason with

For practical purposes, treat IE mode as IE11 with a Microsoft badge. You get no ES modules, no arrow functions, no fetch, no native Promise, no customElements, and a flexbox gap implementation that simply does not exist. document.documentMode reports 11, which is your most reliable in-page signal.

Feature detection is fine, but the detection code itself has to be parseable by the engine. If your probe file uses arrow functions or optional chaining, IE mode throws a syntax error before it can decide anything, and you lose an afternoon wondering why the fallback never fires. Keep the probe in ES5, every time.

Why polyfilling the world is a tax on modern laptops

A full legacy bundle plus polyfills is usually 120–250KB gzipped before your own code shows up. On a fibre connection in a Leeds office that looks free. On a shared laptop in a busy housing association back office, or a nurse's device on a trust network, it is parse time you are spending to serve an audience that is mostly not there any more.

There is a second cost that rarely makes it into the ticket: nobody knows what the polyfill layer contains. It was configured in 2023 by someone who has since moved on, and half the shims were broken anyway. Web components and IntersectionObserver are the usual suspects.

The split is two script tags and a build target

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

Browsers that understand type="module" skip the nomodule script; IE mode skips the module and runs the legacy file. That single pair of tags does most of the work, and it costs nothing at runtime.

Two caveats worth knowing. Some older Safari releases downloaded both files, so a module shim may still be needed if your analytics show Cupertino traffic. And a corporate proxy that rewrites type attributes can confuse the whole arrangement — which is exactly why you want the probe below rather than blind faith.

The build side is unglamorous but important: two Babel targets, two bundle budgets in CI, and a hard cap on the legacy output. If the legacy bundle creeps past, say, 100KB gzipped, the build fails. Nothing disciplines a codebase like a red pipeline.

A probe that earns its keep

(function () {
  var legacy = document.documentMode === 11 ||
    !('noModule' in HTMLScriptElement.prototype);
  var payload = [
    'route=' + (legacy ? 'legacy' : 'modern'),
    'section=' + encodeURIComponent(location.pathname.split('/')[1] || 'root'),
    'dm=' + (document.documentMode || 0)
  ].join('&');
  new Image().src = '/_probe/loader.gif?' + payload;
})();

Defer it, or fire it on load. navigator.sendBeacon does not exist in IE mode, so the one-pixel image trick is still the pragmatic choice. Note what it does not send: the full path, query strings, form values, or anything that could carry personal data into a log you have to explain later.

Logging that survives an information request

Aggregate. Count loader routes per day, per site section, and nothing else. No persistent identifiers, no IP addresses in the application log — your CDN or reverse proxy will already have some of that under its own retention rules, and you do not need a second copy.

What you want out of this is a weekly number you can put in front of a service owner: for the benefits calculator, 4.1% of sessions took the legacy route last month, down from 6.3%. When the figure drops below a threshold you agreed in advance — and stays there for a quarter — you have an evidence-based case to set a retirement date rather than an argument between two teams with strong feelings.

Keep a simple log of when the legacy bundle was last changed and why. If anyone asks why the site still ships code for a browser Microsoft itself has retired, the answer should be one paragraph long, not an archaeology project.

Guardrails before you ship

When to pull the plug

Retirement is a comms job, not a technical one. Publish a plain-English notice, give people a few months, and point them at their IT desk rather than a browser download page they cannot use. The probe data is what lets you pick the date calmly. Build the split now, log quietly, and the awkward meeting about IE mode in 2028 becomes a two-line email instead.

All Articles

Related Articles

The Wrapper, the Pin and the Kill Switch: Extending Vendor Loaders You Cannot Fork

The Wrapper, the Pin and the Kill Switch: Extending Vendor Loaders You Cannot Fork

The Long Weekend Release Drill: Ship on Friday, Sleep on Monday

The Long Weekend Release Drill: Ship on Friday, Sleep on Monday

Forty Lines and a Promise: Swapping a Tag Manager for a Consent-Aware Script Loader

Forty Lines and a Promise: Swapping a Tag Manager for a Consent-Aware Script Loader