o babel 7 и немного больше, Артем Яворский
TRANSCRIPT
Artem Yavorskyhellyeah, Kyiv
🌞
🌞
🌘
🌘
eslint-plugin-compat
🌘
🌘
🌘
It was October…
It was October…
It was October…
First PR
И че? Зачем это?
- Тратишь от 2 до ∞ часов в неделю
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы-
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта
плюсы
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта ✓ Легко обьяснить интервьюверу
плюсы
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта ✓ Легко обьяснить интервьюверу
✓ Легко обьяснить новому клиенту
плюсы
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта ✓ Легко обьяснить интервьюверу
✓ Легко обьяснить новому клиенту ✓ Ведьмак 3 так и не пройден
плюсы
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта ✓ Легко обьяснить интервьюверу
✓ Легко обьяснить новому клиенту ✓ Ведьмак 3 так и не пройден
✓ Есть шанс выйти на opencollective
плюсы
- Тратишь от 2 до ∞ часов в неделю - Сложно обьяснить девушке
- Сложно обьяснить друзьям - Ведьмак 3 так и не пройден
- Материальное разочарование - Все равно этим занимаешься
минусы
Тратить время на OSS
✓ Получаешь от 2 до ∞ часов опыта ✓ Легко обьяснить интервьюверу
✓ Легко обьяснить новому клиенту ✓ Ведьмак 3 так и не пройден
✓ Есть шанс выйти на opencollective ✓ Все равно этим занимаешься
плюсы
JavaScript компилятор
class User { async load() { await fetch('user'); } }
es6
class User { async load() { await fetch('user'); } }
function _instanceof(left, right) { if (right != null && typeof
Symbol !== "undefined" && right[Symbol.hasInstance]) { return
right[Symbol.hasInstance](left); } else { return left instanceof
right; } }
function _asyncToGenerator(fn) { return function () { var self =
this, args = arguments; return new Promise(function (resolve,
reject) { var gen = fn.apply(self, args); function step(key,
arg) { try { var info = gen[key](arg); var value = info.value; }
catch (error) { reject(error); return; } if (info.done)
{ resolve(value); } else { Promise.resolve(value).then(_next,
_throw); } } function _next(value) { step("next", value); }
function _throw(err) { step("throw", err); } _next(); }); }; }
function _classCallCheck(instance, Constructor) { if (!
_instanceof(instance, Constructor)) { throw new
TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i <
props.length; i++) { var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true; if ("value" in descriptor)
descriptor.writable = true; Object.defineProperty(target,
descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if
(protoProps) _defineProperties(Constructor.prototype,
protoProps); if (staticProps) _defineProperties(Constructor,
staticProps); return Constructor; }
var User =
/*#__PURE__*/
function () {
function User() {
_classCallCheck(this, User);
es6
es5
40👍
👍
👍
class User { async load() { await fetch('user'); } }
Sept 28
Babel Turns Three
🎂
110000 websites using transform-classes
Babel on opencollective!
opencollective.com/babel
@
"@ /core": "7.x"
Все babel пакеты теперь scoped
✓ Не надо добавлять каждого участника для каждого пакета ✓ Только команда может паблишить пакеты в определенный scope ✓ Легче отличить “официальные” пакеты от остальных. ✓ Удобно группируются в node_modules
✓ Не надо добавлять каждого участника для каждого пакета ✓ Только команда может паблишить пакеты в определенный scope
✓ Не надо добавлять каждого участника для каждого пакета ✓ Только команда может паблишить пакеты в определенный scope ✓ Легче отличить “официальные” пакеты от остальных.
✓ Не надо добавлять каждого участника для каждого пакета ✓ Только команда может паблишить пакеты в определенный scope ✓ Легче отличить “официальные” пакеты от остальных. ✓ Удобно группируются в node_modules
babel-core
babel-preset-env
babel-plugin-proposal-class-properties
babel-plugin-transform-typescript
babel-runtime
@babel/core
@babel/preset-env
@babel/plugin-proposal-class-properties
@babel/plugin-transform-typescript
@babel/runtime
babel-core
babel-preset-env
babel-plugin-proposal-class-properties
babel-plugin-transform-typescript
babel-runtime
npm info @babel/core version
7.0.0-beta.32
Остановка поддержки node:
< 6 для разработки< 4 для пользователей
ESNext -> ES5
Идея как улучшить javascript
Stage 0
встреча TC39
Stage 1: Proposal
встреча TC39
Stage 2: Draft
встреча TC39
Stage 3: Candidate
встреча TC39
Stage 4: Finished
Stage 4: Finished
Stage 4: Finished
github.com/babel/proposals
Babylon PR New babel plugin
@babel/preset-stage[0-3]
@babel/preset-stage[0-3]изменения в
Class Fields &Static Properties
Private Fields
Class Fields Declarations
Class Fields DeclarationsClass Fields DeclarationsClasss Static FieldsClass Instance Fields
class A { static b = 'foo'; c = ‘bar'; }
class A { constructor() { this.c = 'bar'; } }; A.b = 'foo';
stage 2: @babel/plugin-proposal-class-properties
class A { static b = 'foo'; c = ‘bar'; }
class A { constructor() { this.c = 'bar'; } }; A.b = 'foo';
stage 2: @babel/plugin-proposal-class-properties
class Point { #x = 0;
getFoo() { return this.#x; } }
var _x;
class Foo { constructor() { _x.set(this, 0); }
getFoo() { return _x.get(this); }
} _x = new WeakMap();
PR #6120
class Point { #x = 0;
getFoo() { return this.#x; } }
var _x;
class Foo { constructor() { _x.set(this, 0); }
getFoo() { return _x.get(this); }
} _x = new WeakMap();
PR #6120
class Point { #x = 0;
getFoo() { return this.#x; } }
var _x;
class Foo { constructor() { _x.set(this, 0); }
getFoo() { return _x.get(this); }
} _x = new WeakMap();
PR #6120
class Point { private x; constructor(x) { this.x = x; } }
const point = new Point(2); b.x = 5; // TypeError ?
Why not `private foo` ?
stage 3: @babel/plugin-proposal-object-rest-spread
const symbol = Symbol(1); const obj = { [symbol]: 1, two: 2 };
const {[symbol]: one, ...rest} = obj; console.log(one); // 1 console.log(rest); // { two: 2 }
rest
stage 3: @babel/plugin-proposal-unicode-property-regex
var regex = /\p{Emoji}/u;
regex.test('😊') // true regex.test('hi') // false
github.com/mathiasbynens/regexpu-core/blob/master/property-escapes.md
stage 3: @babel/plugin-proposal-optional-catch-binding
try { throw 0; } catch { doSomething(); }
stage 3: @babel/plugin-proposal-optional-catch-binding
try { throw 0; } catch { doSomething(); }
stage 3: @babel/plugin-proposal-optional-catch-binding
try { throw 0; } catch { doSomething(); }
try { throw 0; } catch (_unused) { doSomething(); }
stage 3: @babel/plugin-syntax-dynamic-import
import('my-script.js').then(data => { console.log(data); });
const data = await import('my-script.js');
stage 3: @babel/plugin-proposal-numeric-separator
100_000_000 // digit 0xA0_B0_C0 // hexadecimal 0o_11_55_21 // octal 0b_11_01 // binary
stage 3: @babel/plugin-proposal-numeric-separator
100_000_000 // digit 0xA0_B0_C0 // hexadecimal 0o_11_55_21 // octal 0b_11_01 // binary
100000000 // digit 0xA0B0C0 // hexadecimal 0o115521 // octal 0b1101 // binary
stage 3: @babel/plugin-proposal-numeric-separator
(0_0) ^0_0/ (0_0)
stage 3: @babel/plugin-proposal-numeric-separator
(0_0) ^0_0/ (0_0) 0
stage 2: @babel/plugin-proposal-decorators
// no parameter decorators (a separate proposal) class Foo { constructor(@foo x) {} }
// no decorators on object methods var o = { @baz foo() {} }
// decorator cannot be attached to the export @foo export default class {}
что теперь нельзя
// no parameter decorators (a separate proposal) class Foo { constructor(@foo x) {} }
// no decorators on object methods var o = { @baz foo() {} }
// decorator cannot be attached to the export @foo export default class {}
что теперь нельзя
// no parameter decorators (a separate proposal) class Foo { constructor(@foo x) {} }
// no decorators on object methods var o = { @baz foo() {} }
// decorator cannot be attached to the export @foo export default class {}
что теперь нельзя
// decorators with a call expression @foo('bar') class A { // decorators on computed methods @autobind [method](arg) {} // decorators on generator functions @deco *gen() {} // decorators with a member expression @a.b.c(e, f) m() {} }
// exported decorator classes export default @foo class {}
что можно
// decorators with a call expression @foo('bar') class A { // decorators on computed methods @autobind [method](arg) {} // decorators on generator functions @deco *gen() {} // decorators with a member expression @a.b.c(e, f) m() {} }
// exported decorator classes export default @foo class {}
что можно
// decorators with a call expression @foo('bar') class A { // decorators on computed methods @autobind [method](arg) {} // decorators on generator functions @deco *gen() {} // decorators with a member expression @a.b.c(e, f) m() {} }
// exported decorator classes export default @foo class {}
что можно
// decorators with a call expression @foo('bar') class A { // decorators on computed methods @autobind [method](arg) {} // decorators on generator functions @deco *gen() {} // decorators with a member expression @a.b.c(e, f) m() {} }
// exported decorator classes export default @foo class {}
что можно
// decorators with a call expression @foo('bar') class A { // decorators on computed methods @autobind [method](arg) {} // decorators on generator functions @deco *gen() {} // decorators with a member expression @a.b.c(e, f) m() {} }
// exported decorator classes export default @foo class {}
что можно
[WIP] stage 1: @babel/plugin-proposal-block-params
[WIP] stage 1: @babel/plugin-proposal-block-params
getSomething('/', () => { // ... })
getSomething('/') { // ... })
[WIP] stage 1: @babel/plugin-proposal-block-params
getSomething('/', () => { // ... })
[WIP] stage 1: @babel/plugin-proposal-block-params
getSomething('/', () => { // ... })
getSomething('/') { // ... })
[WIP] stage 1: @babel/plugin-proposal-block-params
getSomething('/', (err, data) => { // ... })
[WIP] stage 1: @babel/plugin-proposal-block-params
getSomething('/', (err, data) => { // ... })
getSomething('/') do (err, data) { // ... })
[WIP] stage 1: @babel/plugin-proposal-block-params
unless (expr) { // statements }
[WIP] stage 1: @babel/plugin-proposal-block-params
server(app) { ::get('/') do (response) { response.send('hello world'); }
::listen(3000) { console.log('server is running…'); } }
express
[WIP] stage 1: @babel/plugin-proposal-block-params
const heroes = hero { ::name ::height ::mass ::friends { ::name ::home { ::name ::climate } } }
graphql
[WIP] stage 1: @babel/plugin-proposal-block-params
<Box> { select (this.state.step) { ::when ('Intro') { <Intro /> } ::when ('Submit') { <Submit /> } ::when ('Finish') { <Finish /> } } } </Box>;
jsx
stage 1: @babel/plugin-coffeescript-in-js
stage 1: @babel/plugin-optional-chaining
stage 1: @babel/plugin-poposal-optional-chaining
a?.b?.[value].c?.()
a == null ? void 0 : a.b == null ? void 0 : a.b[value].c == null ? void 0 : a.b[value].c.call(a.b[value].c);
interface Props { name: any; enthusiasmLevel?: any; } const props: Props = {};
const props = {};
awesome-typescript-loader ts-loader
awesome-typescript-loader ts-loader
awesome-typescript-loader ts-loader
useBabel: true
awesome-typescript-loader ts-loader
module: { rules: [{ test: /\.ts(x?)$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: babelOptions }, { loader: 'ts-loader' } ] }, { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: babelOptions } ] }] },
useBabel: true
@babel/plugin-transform-typescript
awesome-typescript-loader
ts-loader
{ "plugins": ["@babel/plugin-transform-typescript"] }
{ "presets": [“@babel/preset-typescript"] }
.babelrc
или
>babel index.ts index.js
>webpack index.ts index.js
или
@babel/preset-env
> npm install babel-preset-es2015
> npm install babel-preset-es2015 WARN deprecated [email protected]: We're super 😸 excited that you're trying to use ES2017+ syntax, but instead of making more yearly presets 😭 , Babel now has a better preset that we recommend you use instead: npm install babel-preset-env --save-dev. preset-env without options will compile ES2015+ down to ES5 just like using all the presets together and thus is more future proof. It also allows you to target specific browsers so that Babel can do less work and you can ship native ES2015+ to user 😎 ! We are also in the process of releasing v7, so please give http://babeljs.io/blog/2017/09/12/planning-for-7.0 a read and help test it out in beta! Thanks so much for using Babel 🙏, please give us a follow on Twitter @babeljs for news on Babel, join slack.babeljs.io for discussion/development and help support the project at opencollective.com/babel
{ presets: [['env', { targets: { browsers: "chrome 63, safari 11" }, }]] }
autoprefixer для Babel
class User { async load() { await fetch('user'); } }
function _instanceof(left, right) { if (right != null && typeof Symbol !==
"undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance]
(left); } else { return left instanceof right; } }
function _asyncToGenerator(fn) { return function () { var self = this, args =
arguments; return new Promise(function (resolve, reject) { var gen =
fn.apply(self, args); function step(key, arg) { try { var info = gen[key]
(arg); var value = info.value; } catch (error) { reject(error); return; } if
(info.done) { resolve(value); } else { Promise.resolve(value).then(_next,
_throw); } } function _next(value) { step("next", value); } function
_throw(err) { step("throw", err); } _next(); }); }; }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance,
Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length;
i++) { var descriptor = props[i]; descriptor.enumerable =
descriptor.enumerable || false; descriptor.configurable = true; if ("value"
in descriptor) descriptor.writable = true; Object.defineProperty(target,
descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps)
_defineProperties(Constructor.prototype, protoProps); if (staticProps)
_defineProperties(Constructor, staticProps); return Constructor; }
var User =
/*#__PURE__*/
function () {
function User() {
_classCallCheck(this, User);
}
…
w3schools.com/browsers
92.7% 7.3%
…
function _instanceof(left, right) { if (right != null && typeof Symbol !==
"undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance]
(left); } else { return left instanceof right; } }
function _asyncToGenerator(fn) { return function () { var self = this, args =
arguments; return new Promise(function (resolve, reject) { var gen =
fn.apply(self, args); function step(key, arg) { try { var info = gen[key]
(arg); var value = info.value; } catch (error) { reject(error); return; } if
(info.done) { resolve(value); } else { Promise.resolve(value).then(_next,
_throw); } } function _next(value) { step("next", value); } function
_throw(err) { step("throw", err); } _next(); }); }; }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance,
Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length;
i++) { var descriptor = props[i]; descriptor.enumerable =
descriptor.enumerable || false; descriptor.configurable = true; if ("value"
in descriptor) descriptor.writable = true; Object.defineProperty(target,
descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps)
_defineProperties(Constructor.prototype, protoProps); if (staticProps)
_defineProperties(Constructor, staticProps); return Constructor; }
var User =
/*#__PURE__*/
function () {
function User() {
_classCallCheck(this, User);
}
В 9 из 10 раз клиенты получают это
а было бы неплохо
class User { async load() { await fetch('user'); } }
medium.com/dev-channel/the-cost-of-javascript-84009f51e99e
The Cost of JavaScript
tl;dr: less code = less parse/compile + less transfer + less to decompress
stage 3 plugins with `shippedProposals: true`
before compilation after compilation
for await 22kb 220kb
object rest spread 21kb 65kb
optional-catch-binding 23kb 26kb
unicode-property-regex 21kb 140kb
Compilation result size
polyfills based on code analysis
with useBuiltIns: "usage"
.babelrc.js
const envOptions = { targets: {} };
if (process.env.NODE_ENV === 'development') { envOptions.targets.browsers = 'safari TP'; } else { envOptions.targets.browsers = '>1%'; }
const presets = ['env', envOptions];
export default { presets, }
.babelrc.js
const envOptions = { targets: {} };
if (process.env.NODE_ENV === 'development') { envOptions.targets.browsers = 'safari TP'; } else { envOptions.targets.browsers = '>1%'; }
const presets = ['env', envOptions];
export default { presets, }
.babelrc.js
const envOptions = { targets: {} };
if (process.env.NODE_ENV === 'development') { envOptions.targets.browsers = 'safari TP'; } else { envOptions.targets.browsers = '>1%'; }
const presets = ['env', envOptions];
export default { presets, }
.babelrc.js
const envOptions = { targets: {} };
if (process.env.NODE_ENV === 'development') { envOptions.targets.browsers = 'safari TP'; } else { envOptions.targets.browsers = '>1%'; }
const presets = ['env', envOptions];
export default { presets, }
.babelrc.js
const envOptions = { targets: {} };
if (process.env.NODE_ENV === 'development') { envOptions.targets.browsers = 'safari TP'; } else { envOptions.targets.browsers = '>1%'; }
const presets = ['env', envOptions];
export default { presets, }
.babelrc.js
> time babel src --out-dir lib 1.21s
> time NODE_ENV=production babel src --out-dir lib 2.31s
bundle-utils
Static server
Static server
Static server
useragent
useragent
Static server📦ES6
Static server
useragent
Static server📦ES5
useragent
👍
👍 🤞
[ "> 1%", "last 3 chrome versions, last 1 edge version", "last 1 chrome version, last 1 safari version" ]
.browsers
export default { entry: [
'app/index.js' ], output: { path: path.join('dist', id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [“env”] } }]}
};
webpack.config.js
export default mapConfigToTargets(({ id, browsers }) => { entry: [
‘app/index.js' ], output: { path: path.join('dist', id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [["env", { targets: { browsers }, } } }]}
});
webpack.config.js
export default mapConfigToTargets(({ id, browsers }) => { entry: [
‘app/index.js' ], output: { path: path.join(‘dist’, id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [["env", { targets: { browsers }, } } }]}
});
webpack.config.js
export default mapConfigToTargets(({ id, browsers }) => { entry: [
‘app/index.js' ], output: { path: path.join(‘dist’, id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [["env", { targets: { browsers }, } } }]}
});
webpack.config.js
export default mapConfigToTargets(({ id, browsers }) => { entry: [
‘app/index.js' ], output: { path: path.join(‘dist’, id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [["env", { targets: { browsers }, } } }]}
});
webpack.config.js
export default mapConfigToTargets(({ id, browsers }) => { entry: [
‘app/index.js' ], output: { path: path.join(‘dist’, id), … },
module: {rules: [{ loader: 'babel-loader', query: { "presets": [["env", { targets: { browsers }, } } }]}
});
webpack.config.js
-.. 1 (> 1%) | /.. main.js -.. 2 (last 3 chrome versions, last 1 edge version) | /.. main.js /.. 3 (last 1 chrome version, last 1 safari version) /.. main.js
-.. 1 (> 1%) | /.. main.js 192KB -.. 2 (last 3 chrome versions, last 1 edge version) | /.. main.js 68KB /.. 3 (last 1 chrome version, last 1 safari version) /.. main.js 44KB
-.. 1 (> 1%) | /.. main.js 192KB -.. 2 (last 3 chrome versions, last 1 edge version) | /.. main.js 68KB /.. 3 (last 1 chrome version, last 1 safari version) /.. main.js 44KB
-.. 1 (> 1%) | /.. main.js 192KB -.. 2 (last 3 chrome versions, last 1 edge version) | /.. main.js 68KB /.. 3 (last 1 chrome version, last 1 safari version) /.. main.js 44KB
with-targets parcel index.html
with-targets parcel index.html
parcel -d $BUNDLE_ID index.html
{ targets: { - browsers: "ie 10, >2%, …” + browsers: CURRENT_BUNDLE_BROWSERS } }
babelrc
static server
static server
parse webpack.config.js
static server
parse webpack.config.js
get bundles
static server
parse webpack.config.js
get bundles
generate map with bundle sizes
static server
parse webpack.config.js
get bundles
generate map with bundle sizes
read headers['User-Agent']
static server
parse webpack.config.js
get bundles
generate map with bundle sizes
read headers['User-Agent']
find & send the smallest bundle for current UA
index.js
app.get('*', function response(req, res) {
const bundleId = getBundleIdByUseragent(req.useragent);
const fullPath = path.join(__dirname, 'dist', bundleId, req.url)
res.write(fs.readFileSync(path.join(fullPath))); res.end();
}
index.js
app.get('*', function response(req, res) {
const bundleId = getBundleIdByUseragent(req.useragent);
const fullPath = path.join(__dirname, 'dist', bundleId, req.url)
res.write(fs.readFileSync(path.join(fullPath))); res.end();
}
index.js
app.get('*', function response(req, res) {
const bundleId = getBundleIdByUseragent(req.useragent);
const fullPath = path.join(__dirname, 'dist', bundleId, req.url)
res.write(fs.readFileSync(path.join(fullPath))); res.end();
}
res.header('Vary', 'User-Agent');
не забываем
-.. 0 (> 1%) 196K -.. 1 (last 3 chrome versions, last 1 edge version) 72K /.. 2 (last 1 chrome version, last 1 safari version) 48K
-.. 0 (> 1%) 196K -.. 1 (last 3 chrome versions, last 1 edge version) 72K /.. 2 (last 1 chrome version, last 1 safari version) 48K
60
-.. 0 (> 1%) 196K -.. 1 (last 3 chrome versions, last 1 edge version) 72K /.. 2 (last 1 chrome version, last 1 safari version) 48K
60
🚷
📦60
72KB
📦TP
48KB
📦11
196KB
bundle-utils
Спасибо
Artem Yavorskytwitter.com/yavorsky_github.com/yavorsky