Differential Serving — делаем свой код чище и производительнее

от автора

Всем привет!

Некоторое время назад думали с командой, как оптимизировать наш бандл. Но когда ты поддерживаешь IE или старые браузеры, оптимизация может стать непосильной задачей, так как бандл преобразуется до es3-5, polyfill-ы и т.д.

Бандл весит много, грузится долго. Но почему пользователь, например, последней версии хрома, должен мучиться с долгой загрузкой приложения?

Differential Serving поможет заметно облегчить бандл — это довольно интересный метод оптимизации. Толкового материала по теме нашла маловато, в основном на английских форумах, поэтому решила поделиться своим небольшим исследованием.

Differential Serving на русский примерно переводится как «условная загрузка ресурсов», но мне кажется, английское название более благозвучное и понятное, поэтому дальше буду использовать его.

Видео

Если неохота читать, то можете посмотреть видео моего доклада на HolyJS

Прежде чем начать разговор про 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, для создания двух бандлов:

  1. Бандл с преобразованием до es3-5, polyfills.
    Для старых браузеров
  2. Такой же бандл, но в es6
    Для новых браузеров

Чтобы корректно подключить бандлы с тегом script и разными атрибутами, можно использовать плагины для webpack: html-webpack-multi-build-plugin, webpack-module-nomodule-plugin и т.д.

Как это работает?

С атрибутом module / nomodule мы даем браузеру возможность выбрать, какой бандл для своей работы взять.

И вроде все идет хорошо, пытаемся сделать пробный вариант:

Рис.2. Пример для Safari 10.1

В примере можно увидеть, что некоторые «старые» браузеры ведут себя некорректно и могут загрузить сразу два бандла. А если посмотреть тестовые примеры, то оказывается, что подобных ошибок в браузерах не так уж и мало.

Но если копнуть еще глубже, то вот все виды ошибок в браузерах:

  1. загружает оба бандла и выполняет их;
  2. загружает оба бандла;
  3. загружает «устаревший» бандл и новый бандл — дважды.

Метод был таким многообещающим, но в итоге подкачал с реализацией.
Может есть способ как-то это поправить?

Хак

Можно воспользоваться старым-добрым хаком. Довольно топорный способ, прямо скажем «в лоб», но многие разработчики на английских форумах советуют именно его.

Для нас тут главное, чтобы бандл загружался и исполнялся только один раз.

<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, так как пользователей, использующих его уже мало, а поддержки слишком много)

Используемые и полезные ссылки:

ссылка на оригинал статьи https://habr.com/ru/company/rambler_group/blog/531404/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *