Всем привет!
Некоторое время назад думали с командой, как оптимизировать наш бандл. Но когда ты поддерживаешь IE или старые браузеры, оптимизация может стать непосильной задачей, так как бандл преобразуется до es3-5, polyfill-ы и т.д.
Бандл весит много, грузится долго. Но почему пользователь, например, последней версии хрома, должен мучиться с долгой загрузкой приложения?
Differential Serving поможет заметно облегчить бандл — это довольно интересный метод оптимизации. Толкового материала по теме нашла маловато, в основном на английских форумах, поэтому решила поделиться своим небольшим исследованием.
Differential Serving на русский примерно переводится как «условная загрузка ресурсов», но мне кажется, английское название более благозвучное и понятное, поэтому дальше буду использовать его.
Краткое содержание
Прежде чем начать разговор про Differential Serving и понять принцип его работы, для полного погружения нужно узнать, что такое «модуль» в js. Или вы можете отправиться дальше.
Модуль old-school
Давайте перемотаем на 7-8 лет назад…
И вспомним, какие раньше были конструкции. Если вы смотрели исходный код библиотек или сами их когда-то писали, то вам будет знаком такой код.
; (function() {}())
Эта конструкция называется «модулем» или самовызывающейся функцией. В качестве примера можете посмотреть библиотеку Lodash.
Напомню, что данный метод сделали для создания собственной области видимости, и чтобы код выполнился только один раз при запуске.
Узнать подробнее о методе
В начале и в конце стоят скобки, так как иначе была бы ошибка. Она произойдет потому, что браузер, видя ключевое слово function в основном потоке кода, попытается прочитать Function Declaration, но вызывать «на месте» разрешено только Function Expression.
Но если function идет в составе более сложного выражения, то браузер считает, что это Function Expression, для этого и нужны скобки.
В начале кода находится точка с запятой — это не опечатка, а «защита от дураков». Если получится, что несколько JS-файлов объединены в один (возможно сжаты), и программист забыл поставить точку с запятой перед файлом с библиотекой, то будет ошибка. Так как последняя строка кода «склеится» с модулем.
Зачем скобки вокруг функции?
В начале и в конце стоят скобки, так как иначе была бы ошибка. Она произойдет потому, что браузер, видя ключевое слово function в основном потоке кода, попытается прочитать Function Declaration, но вызывать «на месте» разрешено только Function Expression.
Но если function идет в составе более сложного выражения, то браузер считает, что это Function Expression, для этого и нужны скобки.
Точка с запятой в начале
В начале кода находится точка с запятой — это не опечатка, а «защита от дураков». Если получится, что несколько JS-файлов объединены в один (возможно сжаты), и программист забыл поставить точку с запятой перед файлом с библиотекой, то будет ошибка. Так как последняя строка кода «склеится» с модулем.
Модуль в es6
Через несколько лет использования модуля, разработчики решили включить его в стандарт es6 и добавить дополнительные возможности.
Теперь модули можно загружать друг в друга и использовать директивы export и import, чтобы обмениваться функциональностью, вызывать функции одного модуля из другого.
export отмечает переменные и функции, которые должны быть доступны вне текущего модуля.
import позволяет импортировать функциональность из других модулей.
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// main.js
import {sayHi} from './sayHi.js';
sayHi('John'); // Hello, John!
В объекте import.meta содержится информация о текущем модуле.
Вкратце выделю основные возможности:
- каждый модуль имеет свою собственную область видимости;
- код в нем выполняется только один раз при импорте;
- в модуле всегда используется режим use strict;
- код в нем выполняется в отложенном (deferred) режиме;
- this не определен;
- async работает во встроенных скриптах.
Для использования модуля необходимо явно указать браузеру, что скрипт является модулем, при помощи атрибута type='module'.
Совместимость, «nomodule»
А вот это, на мой взгляд, самая занимательная особенность модулей.
Старые браузеры не понимают атрибут type='module', а скрипты с неизвестным атрибутом type просто игнорируются.
Рис.1. Поддержка браузерами атрибута type='module'.
Но мы можем сделать для старых браузеров «резервный» скрипт при помощи атрибута nomodule.
<script type="module" src="main.js"></script>
<script nomodule src="legacy.js"></script>
Differential Serving
И вот мы плавно подошли к теме Differential Serving. Его основная идея состоит в том, чтобы использовать атрибуты module / nomodule, для создания двух бандлов:
- Бандл с преобразованием до es3-5, polyfills.
Для старых браузеров - Такой же бандл, но в es6
Для новых браузеров
Чтобы корректно подключить бандлы с тегом script и разными атрибутами, можно использовать плагины для webpack: html-webpack-multi-build-plugin, webpack-module-nomodule-plugin и т.д.
Как это работает?
С атрибутом module / nomodule мы даем браузеру возможность выбрать, какой бандл для своей работы взять.
И вроде все идет хорошо, пытаемся сделать пробный вариант:
Рис.2. Пример для Safari 10.1
В примере можно увидеть, что некоторые «старые» браузеры ведут себя некорректно и могут загрузить сразу два бандла. А если посмотреть тестовые примеры, то оказывается, что подобных ошибок в браузерах не так уж и мало.
Но если копнуть еще глубже, то вот все виды ошибок в браузерах:
- загружает оба бандла и выполняет их;
- загружает оба бандла;
- загружает «устаревший» бандл и новый бандл — дважды.
Метод был таким многообещающим, но в итоге подкачал с реализацией.
Может есть способ как-то это поправить?
Хак
Можно воспользоваться старым-добрым хаком. Довольно топорный способ, прямо скажем «в лоб», но многие разработчики на английских форумах советуют именно его.
Для нас тут главное, чтобы бандл загружался и исполнялся только один раз.
<script>
const scriptEl = document.createElement('script');
if ('noModule' in scriptEl) {
scriptEl.src = 'js/main.js';
scriptEl.type = 'module';
} else {
scriptEl.src = 'js/legacy';
scriptEl.defer = true;
}
document.body.appendChild(scriptEl)
</script>
Проверить, что браузер поддерживает nomodule, можно определив, поддерживает ли он атрибут type='module', так что в условии можно использовать любой атрибут.
Альтернативный подход
Также есть и альтернативный подход — использовать пакет browserslist-useragent.
Выглядеть файл будет примерно так
// .browserslistrc file
const express = require('express');
const { matchesUA } = require('browserslist-useragent');
const exphbs = require('express-handlebars');
…
app.use((req, res, next) => {
try {
const ESM_BROWSERS = [
'Edge >= 16',
'Firefox >= 60',
'Chrome >= 61',
'Safari >= 11',
'Opera >= 48',
];
const isModuleCompatible = matchesUA(
req.headers['user-agent'],
{browsers: ESM_BROWSERS, allowHigherVersions: true}
);
res.locals.isModuleCompatible = isModuleCompatible;
} catch (error) {
…
}
next();
}
Кажется, что в этом методе больше контроля, так как можно указать, какая версия браузера какой бандл будет использовать.
Однако есть довольно весомое «НО». Скоро Google уберет из браузера Chrome строку 'user-agent', а вслед за ним последуют и остальные браузеры.
Поэтому есть подозрения, что browserslist-useragent проживет недолго, а ему на смену придет Client Hints API.
Differential serving vs. polyfill service
Первый вопрос, который возникает при знакомстве с Differential Serving — есть ли аналоги?
Более-менее похожий метод — polyfill service. Также есть различные npm-пакеты, которые частично похожи на polyfill service.
Polyfill service — сервис, который принимает запрос на набор функций браузера и возвращает только те полифиллы, которые необходимы запрашивающему браузеру.
<script src='https://polyfill.io/v3/polyfill.min.js'/>
Кратко разберем его плюсы и минусы.
Плюсы:
кэширование полифиллов
доступно для всех браузеров
контроль (user-agent)
Минусы:
не предлагает решения для es6+
дополнительный запрос
содержит ошибки реализации
Минусы тут довольно весомые — не каждая команда захочет привнести в свое приложение дополнительный блокирующий запрос; решение ниже es6, а также ошибки реализации из-за того, что комьюнити не такое большое, а исправляются ошибки медленно.
Но и у Differential Serving есть свои плюсы и минусы.
Плюсы:
оптимизирует транспилирование, полифиллинг
минимум полифиллов
минимум проблем в обслуживании
Минусы:
время кэширования
настройка webpack, babel
увеличивается время сборки
Стоит прокомментировать минусы. Время кэширования зависит от того, как вы настроите свой бандл. А настройка webpack для кого-то тоже может стать проблемой. Время сборки увеличивается, так как нужно собирать два бандла, но можно настроить так, чтобы второй бандл собирался уже перед выкаткой на прод и не тратить на него время.
Результирующее сравнение можно посмотреть в таблице ниже или в статье.
Итог
Как известно, Microsoft в следующем году перестанет поддерживать IE, но это не значит, что разработчики перестанут поддерживать свои приложения под IE. Мем смешной — ситуация страшная :(
Differential Serving кажется многообещающим методом, хоть и со своей спецификой и некоторыми недостатками. Зато он позволяет уменьшить бандл на ~ 20%.
Вернемся к истории о нашей команде: мы хотели оптимизировать бандл, но нужно было поддерживать IE. И вот, найдя Differential Serving, мне хотелось его опробовать на реальном проекте. Поговорила с менеджерами, они долго совещались и в итоге решили отказаться от поддержки IE, так как пользователей, использующих его уже мало, а поддержки слишком много)
Используемые и полезные ссылки:
- Модули через замыкания
- Module в es6
- Исследование по differential Serving с примерами браузеров
- Интересный подкаст
- Differential Serving Pattern
- Differential serving vs. polyfill service: How to best serve modern and legacy browsers
- Differential Serving — Serve legacy code to old browsers and ES6 code to modern browsers
- Differential Serving
DustCn
Девушка-IE в КДПВ рептилоид?
Fedorkov
diogen4212
HEKOT
В 2020 году:
Все тёлки стали тупыми жирными дурами. Ты всё время их меняешь в надежде на то, что новая будет лучше предыдущей. На первое свидание они надевают кружевное бельё, ты женишься, и думаешь, что это надолго, но через год всё снова повторяется. Иногда ты даже думаешь, что лучше вообще без них, но понимаешь, что без них тебе придётся только дрочить.
atrolov
Спасибо за статью, неплохой подход к подстановки бандла для разных эпох барузера, мы у себя для этих целий проверяли на наличие некоторых фич.
P.S. А что за плагин/прога на скрине где написано: Switch browser, Report bug?
fat32elena Автор
Круто, что вам понравилось)
Р.S. это browserstack
Iskin
Указывать точные версии браузеров в
ESM_BROWSERS
не нужно. Браузерслист умеет в запросsupports es6-module
.