Всем привет!
Некоторое время назад думали с командой, как оптимизировать наш бандл. Но когда ты поддерживаешь 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-файлов объединены в один (возможно сжаты), и программист забыл поставить точку с запятой перед файлом с библиотекой, то будет ошибка. Так как последняя строка кода «склеится» с модулем.
Модуль в 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
ссылка на оригинал статьи https://habr.com/ru/company/rambler_group/blog/531404/

Добавить комментарий