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
- The website UserAgentString identifies over 11,000 unique user agent strings
- Fastly uses Varnish
- normalisation API
- Write some custom VCL(varnish)
But, I use nginx
How to?
- nginx custom module
- nginx lua(open resty)
- ngx_http_js_module
njs
- Javascript in nginx
- NGINX 1.11.10 and later
- https://www.nginx.com/blog/introduction-nginscript/
- http://nginx.org/en/docs/http/ngx_http_js_module.html
- http://nginx.org/en/docs/njs/index.html
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&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!
polyfill.io
By odyss
polyfill.io
- 3,281