Babel + core-js + IE = ???

Сегодня будет рассказ про фронтендерский зоопарк. Начну издалека.

Если вы фронт, то вы знаете, что наш код читается многими браузерами. Вы также знаете, что разные браузеры реализуют разные части стандарта языка, а одинаковые части реализуют по-разному. Одно время такая разница в прочтении превращала разработку в ад. Но довольно быстро появились инструменты, «уравнивающие» ваш код таким образом, чтобы во всех браузерах он читался одинаково.

Мы во ВКонтакте местами поддерживаем IE 11, поэтому для нас вопрос кроссбраузерности стоит остро.

Полифиллы

В сети можно найти бесконечное количество полифиллов для почти всего, что придумано в стандарте: Array.prototype.map, Object.keys, Map, Promise, etc.

Полифилл — это кусочек рантайм-кода, который реализует недостающий функционал так, чтобы он во всех браузерах работал одинаково.

Представим, что у нас есть такой код:

const user = { first_name: 'John' };
const userExtended = Object.assign({}, user, { last_name: 'Doe' });

И нам не повезло так, что приходится поддерживать пользователей на IE.

Сейчас самая популярная библиотека полифиллов — это core-js. Чтобы использовать её, что называется, втупую, можно просто сделать импорт всего пакета в самом начале вашего модуля:

import 'core-js';

const user = { first_name: 'John' };
const userExtended = Object.assign({}, user, { last_name: 'Doe' });

core-js весит очень много, поэтому умнее будет импортнуть только то, что вы планируете использовать:

import 'core-js/actual/object/assign';

const user = { first_name: 'John' };
const userExtended = Object.assign({}, user, { last_name: 'Doe' });

Вроде прикольно. Но далеко от совершенства.

Ваше приложение усложняется, команда растёт и в какой-то момент вы начинаете испытывать дискомфорт от того, что на ревью кто-то в очередной раз не заметил, что в код завезли новый модный метод массива, забыв дописать импорт полифилла. Итог — АНДЕЙФАЙНД ИЗ НОТ Э ФАНКШН.

Транспиляторы

Machines must suffer © Омар Хайям Андрей Ситник

Переложить заботу о стабильности кода на машины — отличная затея. Почти всегда. Почти. Babel — прекрасный инструмент. Он позволяет нам писать на современном стандарте языка, делая за нас все необходимые преобразования в ту версию, которую мы ему укажем.

Даже не так. Мы можем сказать бабелю: «мы поддерживаем вот такой набор браузеров, список найдёшь в .browserslistrc, всё, давай!»

Даже не так. Бабель сам посмотрит в этот файл, если он есть в корне проекта.

Причём в первую очередь бабель решает не задачу поиска полифилла для всяких модных методов. Его основная фича — ТРАНСПИЛЯЦИЯ кода. Это когда вы пишете ваш любимый { ...user, last_name: 'Doe' } (оператор, появившийся в es2015), а эта хрень работает в IE 11, который вышел в 2013.

Как так получается? С помощью транспиляции. Всё, что нам нужно сделать — это поставить пару пакетиков:

yarn add @babel/cli @babel/core @babel/preset-env

И создать пару конфигов:

// .babelrc.js

module.exports = {
  "presets": ["@babel/preset-env"]
}
# .browserslist

last 1 chrome version
IE 11

Всё.

Допустим, наш файл выглядит так:

// script.js

const user = { first_name: 'John' };
const userExtended = { ...user, last_name: 'Doe' };

При запуске npx babel script.js в консоли мы увидим следующее:

"use strict";
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var user = {
	first_name: 'John'
};
var userExtended = _objectSpread(_objectSpread({}, user), {}, {
	last_name: 'Doe'
});

То есть Babel понял, что в нашем списке поддерживаемых браузеров затесался отсталый, который не знает о таком операторе. Поэтому он его ТРАНСПИЛИРУЕТ. В транспилированном коде уже нет спреда, есть какой-то свой спред, собранный из говна из es5.

Ещё раз

Транспиляция — это преобразование одной синтаксической конструкции в другую на этапе билда кода.

Полифилл — кусочек рантайм-кода, который докидывает недостающие методы, функции, конструкторы и т.д.

С помощью полифилла нельзя сделать так, чтобы ?? завёлся в IE 11. Потому что это незнакомая синтаксическая конструкция, которую никаким рантайм-кодом не сделать интерпретируемой. Её можно только заменить на другую перед тем, как она попадёт в браузер. Этим и занимается Babel.

Как это работает?

Бабель состоит из плагинов и пресетов.

Каждый плагин отвечает за преобразование конкретной конструкции языка. Есть, например, плагин для преобразования JSX в React.createElement. Или преобразователь любимого всеми ?? в обычный тернарник.

Пресеты — это тупо набор плагинов, объединенных по какому-то признаку. Есть, например, пресет для реакта. Или для тайпскрипта.

Но самый прикольный пресет — это тот, который мы установили.

@babel/preset-env — это умный пресет, который подключает только те плагины, которые нужны, основываясь на браузерах, которые поддерживает конкретный проект. Собсно, для этого мы и положили файлик .browserslistrc рядом с .babelrc.

Давайте для эксперимента уберем IE 11 из браузерлиста и снова запустим бабель.

Смотрите, что получается:

"use strict";
const user = {
	first_name: 'John'
};
const userExtended = { ...user,
	last_name: 'Doe'
};

В коде пресета есть знание о том, что последний хром поддерживает спред оператор. Поэтому, раз мы хотим поддерживать только этот браузер, нет никакого смысла транспилировать этот код.

Это знание получается из пакета caniuse-lite, который находится в зависимостях browserslist, который находится в зависимостях у @babel/preset-env.

Круто? Я считаю, что круто.

Проблемы

С использованием современных конструкций языка вроде разобрались. Бабель будет решать проблему несовместимости за нас.

Но если мы напишем нечто такое:

const user = { first_name: 'John' };
const userExtended = { ...user, last_name: 'Doe' };
console.log(Object.values(userExtended)); // внимание на эту строчку

и запустим бабель, то мы увидим, что Object.values остался нетронутым:

"use strict";
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var user = {
	first_name: 'John'
};
var userExtended = _objectSpread(_objectSpread({}, user), {}, {
	last_name: 'Doe'
});
console.log(Object.values(userExtended)); // внимание на эту строчку

Хотя этот метод появился в es2015 и IE 11 его не поддерживает. То есть этот код в IE упадёт с ошибкой! Как же нам быть?

Babel + core-js = ❤️

В @babel/preset-env есть решение этой проблемы. Давайте чутка подпилим наш конфиг:

// .babelrc.js

module.exports = {
  "presets": [["@babel/preset-env", {
    "useBuiltIns": "usage",
    "corejs": 3
  }]]
}

Если переводить на русский, то написано следующее: «дорогой пресет энв, если ты в коде заметишь современные конструкции, которые можно заполифиллить, то так пожалуйста и сделай. Используй для этого core-js 3-й версии. Спасибо»

Перевод вольный.

Запускаем бабель:

"use strict";
require("core-js/modules/es.object.keys.js");
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.array.filter.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.object.get-own-property-descriptor.js");
require("core-js/modules/web.dom-collections.for-each.js");
require("core-js/modules/es.object.get-own-property-descriptors.js");
require("core-js/modules/es.object.values.js");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var user = {
	first_name: 'John'
};
var userExtended = _objectSpread(_objectSpread({}, user), {}, {
	last_name: 'Doe'
});
console.log(Object.values(userExtended));

Что за дьявольщина, спросите вы? Я вчера задавался тем же вопросом.

Но давайте успокоимся и хладнокровно посмотрим на то, что натворил бабель.

Мы видим, что теперь для Object.values был импортирован полифилл из core-js. То есть этот код будет работать.

Точнее так. Этот код будет работать, когда мы все эти реквайры соберем в один большой JS-файл. Об этом чуть позже.

Но какого чёрта он добавил столько казалось бы лишних полифиллов? Например, полифилл для Object.keys. Это метод из стандарта es5. На сайте caniuse.com написано, что IE 11 его поддерживает. Пресет сошёл с ума?

На самом деле нет.

Дело в том, что core-js, который мы присобачили к пресет энву, имеет собственный мап, в котором чётко описано, с каких версий та и или иная фича поддержана в браузере. И прикол в том, что хоть Object.keys и работает в IE, с точки зрения создателей core-js, он работает там неправильно. Об этом мне поведал мейнтенер бабеля.

То есть сам пресет энв берет инфу о поддержке той или иной конструкции из caniuse-lite, а core-js — из собственного мапа. Веселуха!

Измерения

Ща мы будем собирать этот код вебпаком и смотреть, как меняется размер итогового файла в зависимости от настроек бабеля и наших браузерных предпочтений.

Ставим babel-loader:

yarn add babel-loader

Конфиг вебпака as simple as possible:

// webpack.config.js

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  module: {
    rules: [{
      test: /.js$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }]
  },
}
// src/index.js

const user = { first_name: 'John' };
const userExtended = { ...user, last_name: 'Doe' };
console.log(Object.values(userExtended));

После минимизации такой код весит 64 байта. Запускаем npx webpack

Без автополифиллов

last 2 chrome versions
last 2 safari versions
last 2 opera versions
last 2 edge versions
last 2 firefox versions

134 bytes

То есть вебпак сделал x2, обмазав код своим бойлерплейтом. Терпимо.

last 2 chrome versions
last 2 safari versions
last 2 opera versions
last 2 edge versions
last 2 firefox versions
IE 11

1.2 KiB (x10)

Желание поддерживать IE 11 увеличивает размер бандла в 10 раз. Стерпим и это.

С автополифиллами

last 2 chrome versions
last 2 safari versions
last 2 opera versions
last 2 edge versions
last 2 firefox versions

134 bytes. Логично, так как все перечисленные браузеры ни в каких полифиллах не нуждаются.

last 2 chrome versions
last 2 safari versions
last 2 opera versions
last 2 edge versions
last 2 firefox versions
IE 11
defaults

22.7 KiB (x170). Ну вы поняли. Самая мерзость в том, что даже дефолтные настройки браузерлиста генерят такой огромный кусок JS.

Визуализация

Результат выполнения команды npx webpack --analyzeРезультат выполнения команды npx webpack --analyze

Наш index.js (справа) весит 2 KB, а полифиллы для него — больше 20. И всё ради одного браузера.

Итоги

  • При использовании @babel/preset-env обязательно позаботьтесь о том, чтобы указать список интересующих лично вас браузеров. Иначе ваши пользователи будут скачивать огромную кучу скорее всего ненужного кода.

  • В топку IE.

© Habrahabr.ru