Skip to content

Instantly share code, notes, and snippets.

@betaorbust
Last active July 21, 2018 19:57
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save betaorbust/fed8112f72005a3472d78967b8d121a2 to your computer and use it in GitHub Desktop.
Save betaorbust/fed8112f72005a3472d78967b8d121a2 to your computer and use it in GitHub Desktop.
ES2015 fallback check for Progressive Transpilation

ES2015 Syntax Check for use with Progressive Transpilation

The attached file file spits out a string for direct injection into the head of a page, which will be a lightweight test of the ability to parse and run ES2015 syntax.

Unminified version

This is the original source for the test-case below:

class ಠ_ಠ extends Array{
    constructor(j = "a", ...c) {
        const q = (({u: e}) => {
            return {
                [`s${c}`]: Symbol(j)
            };
        })({});
        super(j, q, ...c);
    }
}
new Promise((f) => {
    const a = function* (){
        return "\u{20BB7}".match(/./u)[0].length === 2 || true;
    };
    for(let vre of a()){
        const [uw, as, he, re] = [new Set(), new WeakSet(), new Map(), new WeakMap()];
        break;
    }
    f(new Proxy({}, {get: (han, h) => h in han ? han[h] : "42".repeat(0o10)}));
}).then(bi => new ಠ_ಠ(bi.rd));

Coverage

This code is a set of ES2015 syntax and features that represent a broad but not deep test case for a browser supporting es2015.

  • arrows functions
  • classes
  • enhanced object literals
  • template strings
  • destructuring
  • default + rest + spread
  • let + const
  • iterators + for..of
  • generators
  • unicode
  • proxies
  • symbols
  • subclassable built-ins
  • promises
  • math + number + string + array + object APIs
  • binary and octal literals

Background

Explanation of how this code is used can be found in the talk Jem and I gave: "Progressive Transpilation at Netflix and the road to running native ES2015 in production".

Video here. The Progressive Transpilation part starts at 25:25:

Progressive Transpilation at Netflix and the road to running native ES2015 in production

Slides here:

Progressive Transpilation at Netflix and the road to running native ES2015 in production slide

Usage

Remember, this isn't a full test suite; the intended use case is to:

  1. Parse the UA string and determine ES2015 eligibility
  2. If ES2015 is indicated, ship a page with ES2015-level code.
  3. Inject the string produced in this file in a script tag at the top of the page which will check to make sure the browser can parse ES2015 syntax and run some of the newer constructs.
  4. If it turns out the browser's UA was a lie, and it can't actually parse or run ES2015, we set a cookie to limit future requests to ES5, and bounce the page as we know the incoming JS package won't work on this browser. Because the cookie is sticky, the interaction happens only once, and is only for corner cases where browsers are lying about their capabilities -- a minority case already.
'use strict';
/*
* The whitespace minified version of the above test case. Note that because many of the
* variables/calculations above are not used, running through a good minifier would
* actually strip out some test cases -- hence, whitespace only minification.
* This is a string, so we don't need to worry about accidentally transpiling
* or minifying it later in a development build.
*/
const es2015Test = 'class ಠ_ಠ extends Array{constructor(j="a",...c){const q=(({u:e})=>{return{[`s${c}`]:Symbol(j)}})({});super(j,q,...c)}}' +
'new Promise(f=>{const a=function*(){return"𠮷".match(/./u)[0].length===2||true};' +
'for(let vre of a()){const[uw,as,he,re]=[new Set,new WeakSet,new Map,new WeakMap];break}' +
'f(new Proxy({},{get:(han,h)=>h in han?han[h]:"42".repeat(8)}))}).then(bi=>new ಠ_ಠ(bi.rd));';
/*
* This is the string to be injected into a <script> tag for attempting to parse
* ES2015 syntax. If parsing/running fails, it will set a cookie which you can pick
* up on the server in subsequent requests. It then bounces the page immediately, as
* ES2015 syntax is coming over the wire, and we know this browser can't handle it.
*/
const testBody = `try{
eval('${es2015Test}');
} catch (e) {
document.cookie = 'esVersion=5; expires=' + (new Date((new Date()).getTime() + 2678400000)).toGMTString() + '; path=/ ;domain=netflix.com';
if(location.reload){location.reload();}else{location.href = location.href;}
}`;
module.exports = testBody;
@benmvp
Copy link

benmvp commented Apr 5, 2017

Soooooo... this is all sorts of awesome!!

The team here at Eventbrite is really loving the idea!

Got a couple of questions for ya.

1/ Did you benchmark how long it took to do the ES2015 test?

2/ It seems like you could set the cookie if it passes as well, and then not have to run the ES2015 test ever again!

3/ Could you also use the info from the cookie to inform the UA lookup that the UA actually doesn't support ES2015? Or is it that the same UA could pass for one person's computer and not for another?

4/ Did you use a library to do the UA lookup or is it something hand-rolled from the ES6 compat table?

5/ Assuming that you're also using ES2016, ES2017 or Stage-X features? Do those also get transpiled or are you doing progressive transpilation for each version of ES?

Thanks again!

(@gagoar & @albybarber)

@betaorbust
Copy link
Author

Little bit of a disclaimer: we're still evaluating it at the moment (which may go on hold due to my upcoming paternity leave) so these are the current answers, but may not be the final answers if/when we roll it out at scale.

  1. We're still looking into perf. It's not the fastest thing in the world, but it's not the worst thing in the world (~1ms on my current machine -- test yours here)
  2. Absolutely! There are a few corner cases (like users syncing cookies across different browsers with differing abilities) but they are very limited, so this might work well. Doing it every time non-ES5 code is shipped is the very conservative route.
  3. In general UAs for identical browser versions don't change. The main place we've seen differences is when a browser manufacturer or plugin masquerades as a different browser to try to get different/better features from a website. That said, UA strings are lies upon lies upon lies, so YMMV ;)
  4. We use a 3rd party UA parser, and then augmented it with a few netflix-specific tricks (video streaming has its own quirks). For our first tests we're going to just white-glove some known large-hitting browsers. I selected our targeted browsers by looking at what ES2015 features we use/plan to use, and selected browsers that met those requirements through Kangax's awesome compat tables.
  5. To start out with this test we're just going to target ES2015 vs ES5. But the general idea with Progressive Transpilation should hold for various levels of ES support, and paves a solid path towards supporting the language as it evolves.

It's not without additional tradeoffs (longer build times for prod; if you're logging uncaught errors, make sure to capture the UA as well; etc.) but not having to ship polyfills/transpile helpers/unnecessarily large transpiled code could be a huge win for the users.

There are certainly some open questions around the performance optimization of ES2015+ in browsers at this time, but even if we have to wait a while for the runtime perf comes up to scratch, or only target really recent browsers, this path forward looks like a good solution for shipping only the minimum amount of complexity over the wire as the language evolves.

@getify
Copy link

getify commented Apr 10, 2017

@unlight
Copy link

unlight commented Jul 21, 2018

@betaorbust
Are you using it in production or still evaluating?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment