Сегодня будет рассказ про фронтендерский зоопарк. Начну издалека.
Если вы фронт, то вы знаете, что наш код читается многими браузерами. Вы также знаете, что разные браузеры реализуют разные части стандарта языка, а одинаковые части реализуют по-разному. Одно время такая разница в прочтении превращала разработку в ад. Но довольно быстро появились инструменты, “уравнивающие” ваш код таким образом, чтобы во всех браузерах он читался одинаково.
Мы во ВКонтакте местами поддерживаем 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 (c)
Омар ХайямАндрей Ситник
Переложить заботу о стабильности кода на машины — отличная затея. Почти всегда. Почти. 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.
Визуализация
Наш index.js
(справа) весит 2 KB, а полифиллы для него — больше 20. И всё ради одного браузера.
Итоги
При использовании
@babel/preset-env
обязательно позаботьтесь о том, чтобы указать список интересующих лично вас браузеров. Иначе ваши пользователи будут скачивать огромную кучу скорее всего ненужного кода.В топку IE.
Комментарии (18)
onegreyonewhite
29.12.2021 01:47Когда пишите web-приложение с поддержкой ослика, не забудьте проверить как оно в нём работает. Особенно если обмазались промисами.
Мы долго смирялись с размером бандла (который, кстати, не так сильно кратно растёт, если вашего кода сильно больше), пока не решили проверить юзабилити в IE. Это невозможно! Поэтому мы решили, что сотрудники банков (кто ж ещё это устаревшее уг использует?) не должны страдать.
ArthurSupertramp Автор
29.12.2021 01:53Если приложение большое, то да, полифиллы не так сильно бросаются в глаза. Однако зачастую они занимают процентов 20 от общего бандла. При том что современные браузеры могут в большинстве случаев обходиться без них.
rock
29.12.2021 02:00+4"corejs": 3
Пожалуйста, не делайте так. Нужно указывать минорную версию
core-js
, т.е.corejs: '3.20'
, иначе будут использоваться модули и данные, что были только во время3.0
- а это апрель 2019, многое с тех пор прошло.То есть сам пресет энв берет инфу о поддержке той или иной конструкции из
caniuse-lite
preset-env
берет инфу изcompat-table
.Наш
index.js
(справа) весит 2 KB, а полифиллы для него — больше 20.В данном, случае спасибо
babel
за хелперы - из-за не самого лучшего варианта использованияObject.getOwnPropertySymbols
грузится полный полифил символа. Большая часть объёма оттуда. Использовался быReflect.ownKeys
- ситуация была бы куда проще. Вообще, в актуальной версии полифил символа слишком часто подгружается не к месту.Мы во ВКонтакте...
Ребята, пожалуйста, используйте актуальную версию библиотеки - со старой возможны серьезные проблемы, да и соберите все копии в один бандл:
rock
30.12.2021 17:45+1@ArthurSupertramp поправьте таки, пожалуйста, версию
core-js
в конфиге во избежание лишних проблем и вопросов у прочитавших данный пост.
MikeLP
29.12.2021 02:37+4Для того чтобы сайт не был громоздким, делают 2 сборки - modern и legacy.
И потом в зависимости от поддержки nomodule аттрибута будет грузится нужный бандл. Соотвественно люди использующиее современный браузер страдают меньше.
i360u
29.12.2021 04:03+2Количество пользователей IE - не постоянное число, оно уверенно уменьшается и уже находится где-то на уровне погрешности измерений. Инвестиции сил и времени в изчезающий рынок - так себе стратегия. Многие крупные вендоры уже давно объявили о завершении поддержки IE в своих продуктах, включая самих MS. Закопайте уже этот труп.
oWeRQ
29.12.2021 13:44+1Поддержка IE только откладывает его окончательное захоронение, а из-за костылей продолжает страдать весь мир, гринписа на них нет.
ColCh
29.12.2021 13:56Привет! Классная статья, спасибо. Не мог пройти мимо :) Мы выбрали немного другой путь и уже раздаём ES2017 в продакшене пару лет. Ненужные полифиллы в ES2017 не включаем.
Подход, правда, устарел, но суть почти та же. Прикладываю выступление, может быть - будет интересно, как это было сделано https://youtu.be/CKbOHn1lJWw?t=7829
Totor0
29.12.2021 17:03-3Для решения этой и многих других проблем придумали typescript, почему бы не использовать его? При том что гибкая конфигурация т.с позволяет без особых проблем за короткое время перевести весь проект на него.
По моему субъективному мнению, в наше время разрабатывать на проекты ванильном JS нужно считать "плохой практикой", т.к ts позволяет избежать огромного ряда проблем которые возникают при разработке средних и крупных проектов на JS.ArthurSupertramp Автор
29.12.2021 17:05+3А как ts решит проблему, которую решают полифиллы? Кажется, что это несвязанные вещи. TS помогает писать более стабильный код, но к кроссбразуерности он, кмк, отношения не имеет.
arvitaly
29.12.2021 19:19-2ArthurSupertramp Автор
30.12.2021 00:14+2Кажется вы путаете транспиляцию с полифиллами. Попробуйте какой-нибудь модный
Object.values
использовать в своём примере. TS либо грохнется с ошибкой (за что ему кстати спасибо), либо оставит такой вызов без изменений.
weslyg
29.12.2021 22:59Так классно что доходчиво и просто описали кто есть кто, и что за что отвечает, я так долго собирал эту картину у себя в голове, а уж новичкам, надеюсь будет очень полезно.
Правда я ожидал что надейтся хитрый способ сделать чего нибудь с бандлом под ie например сбилдить два, и отдавать в зависимости от user-agent, но и за это спасибо.
serhsavchuk
Самое время учить Python