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

cetntytajsa5tru2vw0mxqlejpe.jpeg

Всем привет!

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

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

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

Краткое содержание


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

qck9ultaccp5bcyjupfqvrw931e.png


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

qjbztzqrpnd5kraes-cotrmi3se.png

Рис. 1. Поддержка браузерами атрибута type='module'.

Но мы можем сделать для старых браузеров «резервный» скрипт при помощи атрибута nomodule.



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 мы даем браузеру возможность выбрать, какой бандл для своей работы взять.

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

zx4fwanlw67vvpcenerv5suu_1e.png

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

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

u6awi8h1-wvjg-pv8qbusphmo8y.png

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

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


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

Хак


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

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



Проверить, что браузер поддерживает 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 — сервис, который принимает запрос на набор функций браузера и возвращает только те полифиллы, которые необходимы запрашивающему браузеру.