Polyfill

Me

What is a polyfill?

Code Transform

&

polyfill

const arr = [1, 2, 3]
let transformed = []

if (arr.includes('babel')) {
 transformed = arr.map(num => num * num)
}

original source

"use strict";

var arr = [1, 2, 3];
var transformed = null;

if (arr.includes("babel")) {
  transformed = arr.map(function(num) {
    return num * num;
  });
}

compiled source

npm install --save @babel/polyfill

Polyfill Setup

import "@babel/polyfill";

How to use polyfill

module.exports = {
  entry: ["@babel/polyfill", "./app.js"],
};

webpack config

It should be before all your compiled Babel code

Size does matter

// Cover all standardized ES6 APIs.
import "core-js/es6";

// Standard now
import "core-js/fn/array/includes";
import "core-js/fn/string/pad-start";
import "core-js/fn/string/pad-end";
import "core-js/fn/symbol/async-iterator";
import "core-js/fn/object/get-own-property-descriptors";
import "core-js/fn/object/values";
import "core-js/fn/object/entries";
import "core-js/fn/promise/finally";

// Ensure that we polyfill ES6 compat for anything web-related, if it exists.
import "core-js/web";
import "regenerator-runtime/runtime";
...

@babel/preset-env

  • replaces import '@babel/polyfill' or import 'core-js' to import only required features for the target environment. So, for example, for enough modern target

useBuiltIns: entry

useBuiltIns: entry

import 'core-js';
import 'core-js/modules/es.promise.finally';
import 'core-js/modules/es.string.pad-start';
import 'core-js/modules/es.string.pad-end';
...
  • adds at the top of each file imports of polyfills for features used in this file

useBuiltIns: usage

useBuiltIns: usage

// first file:
var set = new Set();

// second file:
var array = Array.of(1, 2, 3);

original source

useBuiltIns: usage

// first file:
import 'core-js/modules/es.set';
var set = new Set();

// second file:
import 'core-js/modules/es.array.of';
var array = Array.of(1, 2, 3);

compiled source

import 'element-closest'
import 'core-js/es6/promise'
...

Import individual module

What is more effective way?

Polyfill JavaScript Only When You Need To

  • Polyfill.io reads the User-Agent (UA) header of each request and returns polyfills that are suitable for the requesting browser

polyfill.io setup

<script 
src="https://cdn.polyfill.io/v2/polyfill.min.js">
</script>
/* Polyfill service v3.25.1
 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service.
 * 
 * UA detected: chrome/69.0.0
 * Features requested: default
 *  */

(function(undefined) {

/* No polyfills found for current settings */

})
.call('object' === typeof window && window || 
'object' === typeof self && self || 
'object' === typeof global && global || 
{});
/* Polyfill service v3.25.1
 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service.
 * 
 * UA detected: ie/11.0.0
 * Features requested: default
 * 
 * - Object.assign,
 * - Symbol,
 * - Symbol.iterator
 * - Symbol.toStringTag
 * - _Iterator
 * - Object.setPrototypeOf
 * - String.prototype.includes
 * - String.prototype.contains
 * - _ArrayIterator
...

default

https://cdn.polyfill.io/v2/polyfill.min.js
"default":
[
"Array.from","Array.isArray","Array.of","Array.prototype.every",
"Array.prototype.fill","Array.prototype.filter","Array.prototype.forEach",
"Array.prototype.indexOf","Array.prototype.lastIndexOf",
"Array.prototype.map","Array.prototype.reduce",
"Array.prototype.reduceRight","Array.prototype.some",
....
]

feature

https://cdn.polyfill.io/v2/polyfill.min.js
?features=fetch,IntersectionObserver
&excludes=Document

Flags

https://cdn.polyfill.io/v2/polyfill.min.js
?features=fetch,IntersectionObserver&
flags=always,gated

Overriding the user-agent

https://cdn.polyfill.io/v2/polyfill.min.js
?features=fetch,IntersectionObserver&
ua=Mozilla%2F5.0%20(Linux%3B%20
Android%206.0.1%3B%20SM-N910S%20Build%2FMMB29K)%20
AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20
Chrome%2F50.0.2661.102%20Crosswalk%2F20.50.533.55%20
Mobile%20Safari%2F537.36%20NAVER
(inapp%3B%20search%3B%20590%3B%208.8.2)

unkown

https://cdn.polyfill.io/v2/polyfill.min.js
?features=IntersectionObserver&
unknown=polyfill

Feature detection

var features = [];
('Promise' in window) || features.push('Promise');
('IntersectionObserver' in window) || 
features.push('IntersectionObserver');

if (features.length) {
  document.write
  ('<script src="https://cdn.polyfill.io/v2/polyfill.min.js?
  unknown=polyfill&features=' 
  + features.join(',') + '&flags=gated,always">
  <\x2fscript>')
}

no SLA (service level agreement)

Let's make my polyfill.io

components

  • api(based on express)
  • normalize / user agent parse
  • polyfill library

How to serve polyfill?

  • resolveAliases
  • filterForUATargeting(Filter the features object to remove features not suitable for the current UA)
  • resolveDependencies
  • filterForUATargeting
  • filterForExcludes
  • topological sort(feature)
  • output stream

alias

{
  default:
    ["Array.from",
     "Array.of",
     ...
    ]
  es6:
    ["Symbol.species",
     ...
    ]
  ...
}

polyfills/__dist/aliases.json

ua taget / dependency

{
  "aliases": [
  ],
  "browsers": {
    "android": "4.4 - *",
    ...
  },
  "dependencies": [
    "getComputedStyle",
    ...
    ]
}

config.json for each polyfill lib

Topological sort

$ tsort <<EOF
> 3 8
> 3 10
> 5 11
> 7 8
> 7 11
> 8 9
> 11 2
> 11 9
> 11 10
> EOF
//output
7 5 3 11 8 10 2 9

Check Point

  • Cache
  • Multi Process
  • Process management

Text

Caching User-Agent specific responses with Fastly

But, I use nginx
How to?

  • nginx custom module
  • nginx lua(open resty)
  • ngx_http_js_module

njs

Compability

  • ECMAScript 5.1 (strict mode) with some ECMAScript 6

What is not supported yet

  • ES6 let and const declarations
  • arguments array
  • eval
  • new Function() constructor
  • setInterval, setImmediate

setup

sudo apt-get install nginx-module-njs

setup

load_module modules/ngx_http_js_module-debug.so
load_module modules/ngx_http_js_module.so

develop

production

  • in the top‑level ("main") context of the nginx.conf configuration file

setup

js_include conf.d/normalize.js;
js_set $ua normalizeUserAgent;

...
location / {
  ...
  proxy_cache_key $scheme$proxy_host$request_uri$ua;
  ...
}

/etc/nginx/conf.d/default.conf

converting

  • concat all dependencies
  • remove/replace module.exports
  • const / let -> var
  • duplicate local var
  • remove require('lru-cache)
  • remove toString function
  • remove arguments
  • remove unused functions
...
UA.normalize = function(uaString) {
  if (uaString.match(/^\w+\/\d+(\.\d+(\.\d+)?)?$/i)) {
    return uaString.toLowerCase();
  }
  var ua = new UA(uaString);
  return ua.getFamily() + '/' + ua.getVersion();
};

function normalizeUserAgent(req) {
  return UA.normalize(req.headers['user-agent']);
}

normalize.js

const port = process.env.PORT || 3000
...
startService(port, (err, app) => {
  ....
  if (!Number.isInteger(port) && port.startsWith('/')) {
    fs.chmodSync(port, '777')
  }
  ...
})

monkey patch
(for unix domain socket)

/bin/polyfill-service

CMD ["pm2-runtime", "-i", "max", "bin/polyfill-service"]

pm2
(for cluster mode)

/bin/polyfill-service

  • automatic restart
  • cluster mode
  • ...

caveat

  • naver android app(cross walk)
  • samsung internet(5 - 6.2)
  • ...

baseLineVersions

const baseLineVersions = {
  "ie": ">=7",
  "ie_mob": ">=8",
  "chrome": "*",
  "safari": ">=4",
  "ios_saf": ">=4",
  "ios_chr": ">=4",
  "firefox": ">=3.6",
  "firefox_mob": ">=4",
  "android": ">=3",
  "opera": ">=11",
  "op_mob": ">=10",
  "op_mini": ">=5",
  "bb": ">=6",
  "samsung_mob": ">=4"
};

unknown ua

// lib/index.js
const unsupportedUA = ((!ua.meetsBaseline() || 
ua.isUnknown()) && options.unknown !== 'polyfill');

// lib/UA.js
UA.prototype.meetsBaseline = function() {
  return (this.ua.satisfies(
    baseLineVersions[this.ua.family]));
};

UA.prototype.isUnknown = function() {
  return (Object.keys(baseLineVersions).indexOf(
    this.ua.family) === -1) || 
    !this.meetsBaseline();
};

Samsung Internet

Hybird approch

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?
features=IntersectionObserverEntry&amp;flags=gated&unknown=polyfill"></script>
<script>
    var features = [];
    ('IntersectionObserver' in window &&
     'isIntersecting' in 
     window.IntersectionObserverEntry.prototype) || 
     features.push('IntersectionObserverEntry');
    if (features.length) {
      document.write(
      '<script src="https://cdn.polyfill.io/v2/polyfill.min.js' +
      '/v2/polyfill.min.js?ua=chrome/50&features=' +
      features.join(',') + 
      '&flags=gated,always'
      <\x2fscript>')
    }
</script>

Improvement

  • not manual converting
  • webpack transform loader
  • javascript parser(acorn, esprima)

transform-loader

  • browserify transformation loader for webpack
const through = require('through2');
module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        loader: 'transform-loader?0',
        options: {
          transforms: [
            function transform() {
              return through(
                (buffer) => {
                  const result = buffer.split('')
                    .map((chunk) => String.fromCharCode(127 - chunk.charCodeAt(0)));
                  return this.queue(result).join('');
                },
                () => this.queue(null)
              );
            }
          ]
...

Sample doesn't work

...
rules : [
  {
    test : /\.js$/,
    loader: "transform-loader?0"
  }
],
plugins : [
  new webpack.LoaderOptionsPlugin({
    options : {
      transforms : [
        function(file) {
          return through(function(buf) {
            ....
        }
      ]
    }
  })
]

It works

transforms : [
  function(file) {
    let chunks = [];
    return through((buf, encoding, next) => {
      chunks.push(buf.toString('utf8'))
      next();
    }, function(next) {
      let transformed = transform(chunks.join(''));
      this.push(transformed)
      next()
  });
...
]

It works

How to transform

transforms : [
  function(file) {
    let chunks = [];
    return through((buf, encoding, next) => {
      chunks.push(buf.toString('utf8'))
      next();
    }, function(next) {
      let transformed = transform(chunks.join(''));
      this.push(transformed)
      next()
  });
...
]

How to transform

module.exports = function updater() {
  try {
    require('./lib/update').update(function updating(err, results) {
      if (err) {
        ...
        return;
      }
      ...
    });
  } catch (e) {
    ...
  }
};

How to transform

var LRU = require('lru-cache')(5000);
exports.lookup = function lookup(userAgent, jsAgent) {
  var key = (userAgent || '')+(jsAgent || '')
    , cached = LRU.get(key);

  if (cached) return cached;
  LRU.set(key, (
    cached = exports.parse(userAgent, jsAgent)
  ));

  return cached;
};

How to transform

function transform(source, options = {}) {
  const occurences = []
  let f = falafel(source, options, node => {
    if (node.type === 'Identifier' &&
      ['updater', 'lookup'].includes(node.name)) {
      let targetNode = node
      while (targetNode.type !== 'ExpressionStatement') {
        targetNode = targetNode.parent;
      }
      occurences.push(targetNode);
    }
  });
  occurences.forEach(occurence => {
    occurence.update('')
  })
  return String(f)
}

unnecessary dependency

//webpack.config
module.exports = {
...
  externals: /(lru-cache|\.\/lib\/update)/,
...
}

Thank you!

Made with Slides.com