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

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

Мы во ВКонтакте местами поддерживаем 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.

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

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

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

Итоги

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

  • В топку IE.

Комментарии (18)


  1. serhsavchuk
    28.12.2021 23:59
    -5

    Самое время учить Python


  1. Biul
    29.12.2021 00:05
    +2

    А какой процент пользователей сидят в вк на IE 11, если не секрет?


  1. onegreyonewhite
    29.12.2021 01:47

    Когда пишите web-приложение с поддержкой ослика, не забудьте проверить как оно в нём работает. Особенно если обмазались промисами.

    Мы долго смирялись с размером бандла (который, кстати, не так сильно кратно растёт, если вашего кода сильно больше), пока не решили проверить юзабилити в IE. Это невозможно! Поэтому мы решили, что сотрудники банков (кто ж ещё это устаревшее уг использует?) не должны страдать.


    1. ArthurSupertramp Автор
      29.12.2021 01:53

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


    1. nin-jin
      29.12.2021 06:59

      Речь о том, что core-js не только много весит, но и адски тормозит?


      1. jMas
        29.12.2021 17:29

        Есть где посмотреть тесты производительности по сравнению с нейтивом?


  1. 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 - ситуация была бы куда проще. Вообще, в актуальной версии полифил символа слишком часто подгружается не к месту.

    Мы во ВКонтакте...

    Ребята, пожалуйста, используйте актуальную версию библиотеки - со старой возможны серьезные проблемы, да и соберите все копии в один бандл:


    1. rock
      30.12.2021 17:45
      +1

      @ArthurSupertramp поправьте таки, пожалуйста, версию core-js в конфиге во избежание лишних проблем и вопросов у прочитавших данный пост.


  1. MikeLP
    29.12.2021 02:37
    +4

    Для того чтобы сайт не был громоздким, делают 2 сборки - modern и legacy.

    И потом в зависимости от поддержки nomodule аттрибута будет грузится нужный бандл. Соотвественно люди использующиее современный браузер страдают меньше.


  1. i360u
    29.12.2021 04:03
    +2

    Количество пользователей IE - не постоянное число, оно уверенно уменьшается и уже находится где-то на уровне погрешности измерений. Инвестиции сил и времени в изчезающий рынок - так себе стратегия. Многие крупные вендоры уже давно объявили о завершении поддержки IE в своих продуктах, включая самих MS. Закопайте уже этот труп.


    1. oWeRQ
      29.12.2021 13:44
      +1

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


  1. d_zak
    29.12.2021 11:49

    Полезная информация. Спасибо!


  1. ColCh
    29.12.2021 13:56

    Привет! Классная статья, спасибо. Не мог пройти мимо :) Мы выбрали немного другой путь и уже раздаём ES2017 в продакшене пару лет. Ненужные полифиллы в ES2017 не включаем.

    Подход, правда, устарел, но суть почти та же. Прикладываю выступление, может быть - будет интересно, как это было сделано https://youtu.be/CKbOHn1lJWw?t=7829


  1. Totor0
    29.12.2021 17:03
    -3

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

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


    1. ArthurSupertramp Автор
      29.12.2021 17:05
      +3

      А как ts решит проблему, которую решают полифиллы? Кажется, что это несвязанные вещи. TS помогает писать более стабильный код, но к кроссбразуерности он, кмк, отношения не имеет.


      1. arvitaly
        29.12.2021 19:19
        -2

        1. ArthurSupertramp Автор
          30.12.2021 00:14
          +2

          Кажется вы путаете транспиляцию с полифиллами. Попробуйте какой-нибудь модный Object.values использовать в своём примере. TS либо грохнется с ошибкой (за что ему кстати спасибо), либо оставит такой вызов без изменений.


  1. weslyg
    29.12.2021 22:59

    Так классно что доходчиво и просто описали кто есть кто, и что за что отвечает, я так долго собирал эту картину у себя в голове, а уж новичкам, надеюсь будет очень полезно.

    Правда я ожидал что надейтся хитрый способ сделать чего нибудь с бандлом под ie например сбилдить два, и отдавать в зависимости от user-agent, но и за это спасибо.