Skip to main content
Home / Articles /

Differential Serving

There is "futuristic" JavaScript that we can write. "Stage 0" refers to ideas for the JavaScript language that are still proposals. Still, someone might turn that idea into a Babel plugin and it could compile into code that can ship to any browser. For some of these lucky proposals, Stage 0 becomes 1, 2, 3, and, eventually, an official part of the language.

There used to be a point where even the basic features of ES6 were rather experimental. You'd never ship an arrow function to production ‐ you'd compile it to ES5 and ship that instead. But ES6 (aka ES2015, four years ago!) isn't experimental anymore. Its features aren't proposals, drafts, or candidates. They are finished parts of the language, with widespread support.

The main sticking points with browser support are IE <= 11 and Safari <= 9. It's entirely possible you don't support those browsers. In that case, you're free to ship ES6 features to production, and you probably should, as your code will be smaller and more efficient than if you compiled it to ES5. Philip ran some tests and his results suggest both file sizes and parse/eval times can cut in half or better by adopting the new features. However, if you do need to support browsers that lack support, you'll need to compile to ES5, but it doesn't mean you need to ship ES5 to all browsers. That's what "differential serving" is all about.

How do you pull it off? One way, which is enticingly clever, is this pattern I first saw Philip Walton write about:

<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.mjs"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main.es5.js"></script>

Don't let that .mjs stuff confuse you; it's just a made-up file extension that means, "This is a JavaScript file that supports importing ES6 modules" and it is entirely optional. I probably wouldn't even use it.

The concept is great though. We don't have to write fancy JavaScript feature tests and then kick off a network request for the proper bundle ourselves. We can have that split right at the HTML level. I've even seen little libraries use this to scope themselves specifically to modern browsers.

John Stewart recently did some testing on this to see if it did the job we think it's doing and, if so, whether it's doing it well. First, he covers how you can actually make the two bundles, which takes some webpack configuration. Then he tested to see if it actually worked.

The good news is that most browsers — particularly newer ones — behave perfectly well with differential serving. But there are some that don't. Safari 10 (2016) is a particularly bad offender in that it downloads and executes both versions. Firefox 59 (2018) and IE 11 download both (but execute the correct one) and Edge 18 somehow downloads both versions, then downloads the modules version again. All browsers that are going away rather quickly, but not to be ignored. Still worth doing? Probably. I'd be interested in looking at alternate techniques that fight against these pitfalls.

icon-link icon-logo-star icon-search icon-star