DEV Community

Cover image for uwc part 5: Client side polyfill detection

uwc part 5: Client side polyfill detection

btopro profile image Bryan Ollendyke ・4 min read

Bundling to a single built "app" helps skip the issue of polyfills. In most instances you compile things using babel to ES5, ship it to the web vi an "entrypoint" file like <script src="/dist/app.js"></script> and then you're app loads.

As I've laid out, we do things a bit differently because of unbundling. This enables us to ship people something based on their platform. But how best to do that? Definitely this can be detected server side and then ship ES5 if headers are IE11; but can a "normal" person set this up?

As one of the goals with HAXTheWeb is to empower anyone to be creative, our team didn't like server configured solutions. They are intentionally limiting of the amount of humanity able to participate. It is a technical elitist solution to a low-tech problem.

Client side detecting 3 configurations

After research, I decided to ship to three target browser configurations:

  • ES5-AMD (Firefox ~52 / Safari 9.1 / IE11 / Old Edge)
  • ES6-AMD (Firefox ~60 / Safari 10.1)
  • ES8 (Evergreen / anything 2017ish and up)

Based on Stat counter browser data I estimate the following capabilities breakdown in who gets what:

  • ES8 / evergreen - ~90%
  • ES6-AMD - ~3%
  • ES5-AMD - ~7%

You will have around 0.04% of traffic you can't hit with ES5 even, but that's been a decision tree in front end development for awhile and not a difference (but worth noting).

Detecting ES8 support

There is heavy alignment (through luck) in the dynamic import() spec and ES8. It's not perfect, but we require dynamic imports for web components to really flourish so I selected that as my feature detection cut off. You can detect that feature like this:

try {
  new Function("import('');");
  // if you get here in execution, it's valid ES8
catch(e) {
  // need to serve something else

You can see this in "the magic script" early on (see last post). The new Function call is required because otherwise import() resolves as invalid syntax on older browsers. The try catch block allows this to work on new things (remember, like 90%) while not failing on the other 10% because it is valid syntax.

Now, ES6 with AMD modules

This script is what I'll unpack next in segments. You can read the whole thing here:

It's in part very unreadable because it inserts a babel generated polyfill to ensure that define calls work (the AMD portion of the execution chain).

try {
  if (typeof Symbol == "undefined") { // IE 11, at least try to serve a watered down site
    ancient = true;
  new Function("let a;"); // bizarre but needed for Safari 9 bc of when it was made
catch (err) {
  ancient = true;

The comments on this explain it but we use a symbol check for IE 11 is one that I personally dump because we don't have to support IE11. If you do, take this part out (though you definitely have different considerations as far as how well things like shadowDom work when polyfill'ed that heavily).

The next interesting hack is another new Function call, this time to test for the let variable declaration. This is because Safari 9 is also one that I mark as ancient. If you want to support Safari 9 (which is now 4 versions behind the latest) then drop this piece.

In these attempts to discover something "ancient" it's because we have built in support for redirecting to a page that says "please upgrade your browser", seen at the bottom of the script:

if (window.__appForceUpgrade) {
    window.location = "assets/upgrade-browser.html";

If we say you MUST upgrade, then we kick you over for these browsers.

Determining which polyfills to ship

So, if we're not ancient and we're not evergreen, we're one of our two build targets. This would be like Firefox 52 - 67, Safari 10/11, Edge pre-chromium, and very old versions of Chrome.

Here's the chunk after our babel junk for define:

// FF 6x.x can be given ES6 compliant code safely
  if (!/Safari/.test(navigator.userAgent) && window.customElements) {
    defs = [
      cdn + "assets/babel-top.js",
      cdn + "build/es6-amd/node_modules/web-animations-js/web-animations-next-lite.min.js",
      cdn + "build/es6-amd/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js",
      cdn + "build/es6-amd/node_modules/@polymer/polymer/polymer-legacy.js"
    window.WCAutoloadPolyfillEntryPoint = cdn + "build/es6-amd/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
  else {
    defs = [
      cdn + "assets/babel-top.js",
      cdn + "build/es5-amd/node_modules/web-animations-js/web-animations-next-lite.min.js",
      cdn + "build/es5-amd/node_modules/fetch-ie8/fetch.js",
      cdn + "build/es6/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
      cdn + "build/es5-amd/node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js",
      cdn + "build/es5-amd/node_modules/@polymer/polymer/polymer-legacy.js"
    window.WCAutoloadPolyfillEntryPoint = cdn + "build/es5-amd/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
  define(defs, function () {"use strict";
    define([cdn + "build/es5-amd/node_modules/@lrnwebcomponents/deduping-fix/deduping-fix.js", window.WCAutoloadPolyfillEntryPoint], function () {"use strict";

In this section we check for supporting window.customElements which some browsers DID support before adopting all of ES6. We use this to either server you the legacy version (ES5-AMD) or the slightly faster version ES6-AMD. The ES6-AMD version is like Firefox 63-66.

How we compile

Now that we see how the script handles client side feature detection (something you can implement in your own routines without the whole script!) now we get into how we do the "build" step.

While we're not bundling, we do still need to compile using babel in order to hit those 3 browsing targets. In the next post I'll cover how we use Polymer's CLI tool to unbundle and compile web components regardless of library they were written in (or none at all)!

Discussion (0)

Editor guide