Smart Bundling: как обслуживать устаревший код только для устаревших браузеров
Опубликовано: 2022-03-10Веб-сайт сегодня получает большую часть своего трафика от вечнозеленых браузеров, большинство из которых хорошо поддерживают ES6+, новые стандарты JavaScript, новые API-интерфейсы веб-платформы и атрибуты CSS. Тем не менее, устаревшие браузеры по-прежнему нуждаются в поддержке в ближайшем будущем — их доля использования достаточно велика, чтобы ее нельзя было игнорировать, в зависимости от вашей пользовательской базы.
Беглый взгляд на таблицу использования caniuse.com показывает, что вечнозеленые браузеры занимают львиную долю рынка браузеров — более 75%. Несмотря на это, нормой является префикс CSS, перенос всего нашего JavaScript в ES5 и включение полифиллов для поддержки каждого пользователя, о котором мы заботимся.
Хотя это понятно из исторического контекста — сеть всегда была ориентирована на прогрессивное улучшение — остается вопрос: замедляем ли мы работу сети для большинства наших пользователей, чтобы поддерживать уменьшающийся набор устаревших браузеров?
Стоимость поддержки устаревших браузеров
Давайте попробуем понять, как различные этапы типичного конвейера сборки могут увеличить вес наших внешних ресурсов:
Транспиляция в ES5
Чтобы оценить, насколько транспиляция может увеличить вес пакета JavaScript, я взял несколько популярных библиотек JavaScript, изначально написанных на ES6+, и сравнил размеры их пакетов до и после транспиляции:
Библиотека | Размер (уменьшенный ES6) | Размер (уменьшенный ES5) | Разница |
---|---|---|---|
TodoMVC | 8,4 КБ | 11 КБ | 24,5% |
Перетаскиваемый | 53,5 КБ | 77,9 КБ | 31,3% |
Люксон | 75,4 КБ | 100,3 КБ | 24,8% |
Видео.js | 237,2 КБ | 335,8 КБ | 29,4% |
PixiJS | 370,8 КБ | 452 КБ | 18% |
В среднем нетранспилированные пакеты примерно на 25% меньше, чем те, которые были транспилированы в ES5. Это неудивительно, учитывая, что ES6+ обеспечивает более компактный и выразительный способ представления эквивалентной логики, а перенос некоторых из этих функций в ES5 может потребовать много кода.
Полифилы ES6+
В то время как Babel хорошо справляется с применением синтаксических преобразований к нашему коду ES6+, встроенные функции, представленные в ES6+, такие как Promise
, Map
и Set
, а также новые методы массива и строки, все еще нуждаются в полифилле. Добавление babel-polyfill
как есть может добавить около 90 КБ к вашему уменьшенному пакету.
Полифилы веб-платформы
Разработка современных веб-приложений упростилась благодаря наличию множества новых API браузеров. Обычно используются fetch
для запроса ресурсов, IntersectionObserver
для эффективного наблюдения за видимостью элементов и спецификация URL
, которая упрощает чтение и манипулирование URL-адресами в Интернете.
Добавление полифилла, соответствующего спецификации, для каждой из этих функций может заметно повлиять на размер пакета.
Префикс CSS
Наконец, давайте посмотрим на влияние префиксов CSS. Хотя префиксы не добавят такой массы пакетам, как другие преобразования сборки, особенно потому, что они хорошо сжимаются при Gzip-сжатии, здесь все же можно добиться некоторой экономии.
Библиотека | Размер (уменьшено, с префиксом для последних 5 версий браузера) | Размер (уменьшено, с префиксом для последней версии браузера) | Разница |
---|---|---|---|
Начальная загрузка | 159 КБ | 132 КБ | 17% |
Бульма | 184 КБ | 164 КБ | 10,9% |
фундамент | 139 КБ | 118 КБ | 15,1% |
Семантический интерфейс | 622 КБ | 569 КБ | 8,5% |
Практическое руководство по созданию эффективного кода
Наверное, очевидно, к чему я клоню. Если мы воспользуемся существующими конвейерами сборки, чтобы предоставить эти уровни совместимости только тем браузерам, которые в них нуждаются, мы сможем предоставить более легкий опыт остальным нашим пользователям — тем, кто составляет растущее большинство — при сохранении совместимости со старыми браузерами.
Эта идея не совсем нова. Такие сервисы, как Polyfill.io, представляют собой попытки динамического полифилла в средах браузера во время выполнения. Но такие подходы имеют несколько недостатков:
- Выбор полифилов ограничен теми, которые перечислены сервисом, если только вы не размещаете и не поддерживаете сервис самостоятельно.
- Поскольку полифиллинг происходит во время выполнения и является блокирующей операцией, время загрузки страницы может быть значительно выше для пользователей старых браузеров.
- Предоставление каждому пользователю пользовательского файла полифилла вводит энтропию в систему, что затрудняет устранение неполадок, когда что-то идет не так.
Кроме того, это не решает проблему веса, добавленного транспиляцией кода приложения, который иногда может быть больше, чем сами полифиллы.
Давайте посмотрим, как мы можем решить все источники раздувания, которые мы определили до сих пор.
Инструменты, которые нам понадобятся
- Вебпак
Это будет наш инструмент сборки, хотя процесс останется таким же, как и в других инструментах сборки, таких как Parcel и Rollup. - Список браузеров
При этом мы будем управлять и определять браузеры, которые мы хотели бы поддерживать. - И мы будем использовать некоторые плагины поддержки Browserslist .
1. Определение современных и устаревших браузеров
Во-первых, мы хотим прояснить, что мы подразумеваем под «современными» и «устаревшими» браузерами. Для простоты обслуживания и тестирования это помогает разделить браузеры на две отдельные группы: добавление браузеров, которые практически не требуют полифилла или транспиляции, в наш современный список, а остальных — в наш устаревший список.
Эта информация может храниться в конфигурации Browserslist в корне вашего проекта. Подразделы «Окружающая среда» могут использоваться для документирования двух групп браузеров, например:
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
Приведенный здесь список является лишь примером и может быть настроен и обновлен в зависимости от требований вашего веб-сайта и имеющегося времени. Эта конфигурация будет служить источником правды для двух наборов интерфейсных пакетов, которые мы создадим далее: один для современных браузеров и один для всех остальных пользователей.
2. Транспиляция и полифиллинг ES6+
Чтобы преобразовать наш JavaScript с учетом среды, мы собираемся использовать babel-preset-env
.
Давайте инициализируем файл .babelrc
в корне нашего проекта следующим образом:
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
Включение флага useBuiltIns
позволяет Babel выборочно полифилить встроенные функции, которые были представлены как часть ES6+. Поскольку он фильтрует полифиллы, чтобы включить только те, которые требуются среде, мы снижаем стоимость доставки с помощью babel-polyfill
в полном объеме.
Чтобы этот флаг работал, нам также нужно импортировать babel-polyfill
в нашу точку входа.
// In import "babel-polyfill";
Это заменит большой импорт babel-polyfill
гранулярным импортом, отфильтрованным средой браузера, на которую мы ориентируемся.
// Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …
3. Полифилинг функций веб-платформы
Чтобы предоставить нашим пользователям полифиллы для функций веб-платформы, нам потребуется создать две точки входа для обеих сред:
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
И это:
// polyfills for modern browsers (if any) require('intersection-observer');
Это единственный шаг в нашем потоке, который требует определенной степени ручного обслуживания. Мы можем сделать этот процесс менее подверженным ошибкам, добавив в проект eslint-plugin-compat. Этот плагин предупреждает нас, когда мы используем функцию браузера, которая еще не была полифиллирована.
4. Префикс CSS
Наконец, давайте посмотрим, как мы можем сократить префиксы CSS для браузеров, которым они не требуются. Поскольку autoprefixer
был одним из первых инструментов в экосистеме, который поддерживал чтение из файла конфигурации списка browserslist
, нам здесь особо нечего делать.
Достаточно создать простой файл конфигурации PostCSS в корне проекта:
module.exports = { plugins: [ require('autoprefixer') ], }
Собираем все вместе
Теперь, когда мы определили все необходимые конфигурации плагинов, мы можем собрать конфигурацию веб-пакета, которая считывает их и выводит две отдельные сборки в папках dist/modern
и dist/legacy
.
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };
Чтобы закончить, мы создадим несколько команд сборки в нашем файле package.json
:
"scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }
Вот и все. Выполнение yarn build
теперь должно дать нам две сборки, которые эквивалентны по функциональности.
Предоставление пользователям правильного пакета
Создание отдельных сборок помогает нам достичь только первой половины нашей цели. Нам все еще нужно определить и предоставить пользователям правильный пакет.
Помните конфигурацию списка браузеров, которую мы определили ранее? Было бы неплохо, если бы мы могли использовать ту же конфигурацию, чтобы определить, к какой категории относится пользователь?
Введите список браузеров-useragent. Как следует из названия, browserslist-useragent
может прочитать конфигурацию нашего списка browserslist
, а затем сопоставить пользовательский агент с соответствующей средой. Следующий пример демонстрирует это на сервере Koa:
const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });
Здесь установка флага allowHigherVersions
гарантирует, что если будут выпущены более новые версии браузера — те, которые еще не являются частью базы данных Can I Use — они по-прежнему будут отображаться как правдивые для современных браузеров.
Одна из функций browserslist-useragent
заключается в обеспечении учета особенностей платформы при сопоставлении пользовательских агентов. Например, все браузеры на iOS (включая Chrome) используют WebKit в качестве базового движка и будут сопоставлены с соответствующим запросом Browserslist для Safari.
Возможно, было бы неразумно полагаться исключительно на правильность синтаксического анализа пользовательского агента в рабочей среде. Возвращаясь к устаревшему пакету для браузеров, которые не определены в современном списке или имеют неизвестные или неразборчивые строки пользовательского агента, мы гарантируем, что наш веб-сайт по-прежнему работает.
Вывод: стоит ли?
Нам удалось охватить сквозной процесс доставки пакетов без раздувания нашим клиентам. Но разумно задаться вопросом, стоят ли накладные расходы на обслуживание, которые это добавляет к проекту, его выгоды. Оценим плюсы и минусы такого подхода:
1. Обслуживание и тестирование
Один требуется для поддержки только одной конфигурации Browserslist, которая поддерживает все инструменты в этом конвейере. Обновление определений современных и устаревших браузеров может быть выполнено в любое время в будущем без необходимости рефакторинга поддерживающих конфигураций или кода. Я бы сказал, что это делает накладные расходы на обслуживание практически незначительными.
Однако существует небольшой теоретический риск, связанный с использованием Babel для создания двух разных пакетов кода, каждый из которых должен нормально работать в соответствующей среде.
Хотя ошибки из-за различий в пакетах могут быть редкими, мониторинг этих вариантов на наличие ошибок должен помочь выявить и эффективно устранить любые проблемы.
2. Время сборки и время выполнения
В отличие от других методов, распространенных сегодня, все эти оптимизации происходят во время сборки и невидимы для клиента.
3. Постепенное увеличение скорости
Работа пользователей в современных браузерах становится значительно быстрее, в то время как пользователи старых браузеров продолжают получать тот же пакет, что и раньше, без каких-либо негативных последствий.
4. Простое использование современных функций браузера
Мы часто избегаем использования новых функций браузера из-за размера полифиллов, необходимых для их использования. Иногда мы даже выбираем меньшие полифилы, не соответствующие спецификациям, чтобы сэкономить на размере. Этот новый подход позволяет нам использовать полифиллы, соответствующие спецификациям, не беспокоясь о том, что они повлияют на всех пользователей.
Дифференциальный пакет, работающий в производстве
Учитывая значительные преимущества, мы внедрили этот конвейер сборки при создании новой мобильной кассы для клиентов Urban Ladder, одного из крупнейших в Индии розничных продавцов мебели и предметов декора.
В нашем уже оптимизированном пакете мы смогли сэкономить примерно 20% ресурсов CSS и JavaScript, отправляемых по сети современным мобильным пользователям. Поскольку более 80% наших ежедневных посетителей используют эти вечнозеленые браузеры, затраченные усилия окупились.
Дополнительные ресурсы
- «Загрузка полифиллов только при необходимости», Филип Уолтон
-
@babel/preset-env
Умный пресет Babel - Список браузеров «Инструменты»
Экосистема плагинов, созданная для Browserslist - Могу ли я использовать
Текущая таблица доли рынка браузера