«Шакал»: сжимаем фронтенд

Привет! Я — Ваня, лид платформенной команды в Тинькофф Бизнес.

Мое любимое занятие — открывать вкладку DevTools и проверять, сколько весят артефакты сайта. В этой статье расскажу, как мы сократили вес приложения на 30% силами платформенной фронтенд-команды за один день без изменения кода сайта. Никаких хитростей и регистраций — только nginx, docker и node.js (опционально).

88xlfoc-7kpdci8ptgwy-shah2e.png

Зачем


Сейчас фронтенд-приложения весят немало. Собранные артефакты могут весить 2—3 Мб, а то и больше. Однако пользователям на помощь приходят алгоритмы сжатия.

До недавнего времени мы использовали только Gzip, который был представлен миру еще в 1992 году. Наверное, это самый популярный алгоритм сжатия в вебе, его поддерживают все браузеры выше IE 6.

Напомню, что уровень сжатия у Gzip изменяется в диапазоне от 1 до 9 (больше — эффективнее), а сжимать можно либо «на лету», либо статически.

  • «На лету» (динамически) — артефакты хранятся в полученном после сборки виде, их сжатие происходит во время выдачи на клиент. В нашем случае на уровне nginx.
  • Статически — артефакты после сборки сжимаются, а HTTP-сервер выдает их на клиент «как есть».


Очевидно, что первый вариант требует больше ресурсов сервера на каждый запрос. Второй же — на этапе сборки и подготовки приложения.

Наш фронтенд сжимался динамически четвертым уровнем. Продемонстрирую разницу между сжатым артефактом и исходным:


Можно заметить, что даже четвертый уровень сокращает размер артефакта в 4 раза! А разница между четвертым уровнем и девятым составляет 35 Кб, то есть 1,3% от исходного, но в 2 раза увеличивается время сжатия.

И вот недавно мы задумались: почему бы не перейти на Brotli? Да еще и на самый мощный уровень сжатия!

К слову, этот алгоритм был представлен Google в 2015 году и имеет 11 уровней сжатия. При этом четвертый уровень Brotli эффективнее девятого у Gzip. Я замотивировался и быстро накидал код для сжатия артефактов алгоритмом Brotli. Результаты представлены ниже:


Однако из таблицы видно, что даже первый уровень сжатия Brotli выполняется дольше, чем девятый у Gzip. А последний уровень — аж 8,3 секунды! Это насторожило меня.

С другой стороны, результат однозначно впечатляет. Далее я попробовал перенести сжатие на nginx — загуглил документацию. Все оказалось предельно просто:

brotli on;
brotli_comp_level 11;
brotli_types text/plain text/css application/javascript;


Собрал докер-образ, запустил контейнер и был ужасно удивлен:

o5mkp_c6uyhyrye2i7oq3eok5qk.png

Время загрузки моего файла выросло в десятки раз — со 100 мс до 5 секунд! Приложением стало невозможно пользоваться.

Изучив документацию глубже, понял, что можно раздавать статически. Воспользовался ранее написанным скриптом, сжал те же артефакты, положил в контейнер, запустил. Время загрузки вернулось в норму — победа! Однако радоваться рано, потому что доля браузеров, поддерживающих этот тип сжатия, — около 80%.

Это означает, что необходимо сохранить обратную совместимость, при этом дополнительно хочется использовать самый эффективный уровень Gzip. Так появилась идея сделать утилиту по сжатию файлов, которая позже получила название «Шакал».

oadz7ljyuu-vouck3fsoafeulis.jpeg

Что нам понадобится?


Nginx, Docker и Node.js, хотя при желании можно и на bash.
С Nginx почти все понятно:

brotli off;
brotli_static on;
gzip_static on;


Но что делать с приложениями, которые еще не успели обновить докер-образ? Правильно, добавить обратную совместимость:

gzip on;
gzip_level 4;
gzip_types text/plain text/css application/javascript;


Объясню принцип работы:

mewjg1g0apbwqca2pki2niud9vi.png

Клиент при каждом запросе передает заголовок Accept-Encoding, в котором перечисляет через запятую поддерживаемые алгоритмы сжатия. Обычно это deflate, gzip, br.

Если у клиента в строке есть br, то nginx ищет файлы с расширением .br, если таких файлов нет и клиент поддерживает Gzip, то ищет .gz. Если таких файлов нет, то пожмет «на лету» и отдаст с четвертым уровнем компрессии.

Если клиент не поддерживает ни один тип сжатия, то сервер выдаст артефакты в исходном виде.

Однако возникла проблема: наш докер-образ nginx не поддерживает модуль Brotli. За основу я взял готовый докер-образ.

Dockerfile для «запаковки» nginx в проекте
FROM fholzer/nginx-brotli

# предварительно очищаем директорию с контентом
RUN rm -rf /usr/share/nginx/html/

# копируем нашу конфигурацию в образ
COPY app/nginx /etc/nginx/conf.d/

# копируем наши артефакты в образ
COPY dist/ /usr/share/nginx/html/

# запускаем
CMD nginx -c /etc/nginx/conf.d/nginx.conf


С балансировкой трафика разобрались, но откуда взять артефакты? Вот здесь-то и придет на помощь «Шакал».

«Шакал»


Это утилита для сжатия статики вашего приложения.

Сейчас это три node.js-скрипта, обернутые в докер-образ с node: alpine. Пробежимся по скриптам.

base-compressor — скрипт, который реализует базовую логику по сжатию.

Аргументы на вход:

  1. Функция сжатия — любая javascript-функция, можно реализовать свой алгоритм сжатия.
  2. Параметры сжатия — объект с параметрами, необходимыми для переданной функции.
  3. Расширение — расширение артефактов сжатия. Необходимо указывать начиная с символа точки.


gzip.js — файл с вызовом base-compressor с переданной функцией Gzip из пакета zlib и указанием девятого уровня компрессии.

brotli.js — файл с вызовом base-compressor с переданной функцией Brotli из одноименного npm-пакета и указанием 11-го уровня компрессии.

Dockerfile создания образа «Шакала»
FROM node:8.12.0-alpine

# копируем скрипты в образ
COPY scripts scripts

# копируем package.json и package-lock.json в образ
COPY package*.json scripts/

# задаем рабочую директорию в образе
WORKDIR scripts

# выполняем установку модулей
# эта установка оставит node_modules/ в образе
# можно оптимизировать, если собрать скрипт предварительно
RUN npm ci

# выполняем параллельно два скрипта
CMD node gzip.js | node brotli.js


Разобрались, как он работает, теперь можно смело запускать:

docker run \
   -v $(pwd)/dist:/scripts/dist \
   -e 'dirs=["dist/"]' \
   -i mngame/shakal


  • -v $(pwd)/dist:/scripts/dist — указываем, какую локальную директорию считать директорией в контейнере (ссылка на маунтинг). Указание директории scripts обязательно, так как она является рабочей внутри контейнера.
  • -e 'dirs=[«dist/»]' — указываем параметр окружения dirs — массив строк, которые описывают директории внутри scripts/, которые будут сжаты.
  • -i mngame/shakal — указание образа с docker.io.


В указанных директориях скрипт рекурсивно сожмет все файлы с указанными расширениями .js, .json, .html, .css и сохранит рядом файлы с расширениями .br и .gz. На нашем проекте этот процесс занимает около двух минут при весе всех артефактов около 6 Мб.

На этом моменте, а может быть, и раньше вы могли подумать: «Какой докер? Какая нода? Почему бы просто не добавить два пакета к себе в package.json проекта и вызывать прямо на postbuild?»

Лично мне очень больно видеть, когда ради прогона линтеров в CI проект устанавливает себе 100+ пакетов, из которых ему на этапе линтинга нужны максимум 10. Это время агента, ваше время, как никак time to market.

В случае с докером мы получаем заранее собранный образ, в котором установлено все необходимое именно для сжатия. Если вам сейчас не нужно ничего сжимать — не сжимайте. Нужен линт — прогоняйте только его, нужны тесты — прогоняйте только их. Плюс мы получаем хорошее версионирование «Шакала»: нам не нужно обновлять его зависимости в каждом проекте — достаточно выпустить новую версию, а проекту — использовать latest-тег.

Результат:


  • Размер артефактов изменился с 636 Кб до 446 Кб.
  • Процентно размер уменьшился на 30%.
  • Время загрузки уменьшилось на 10—12%.
  • Время на декомпрессию, исходя из статьи, осталось прежним.


Итого


Помочь своим пользователям можно прямо сейчас, прямо следующим ПРом: добавляете шаг после сборки — сжатие «Шакалом», после чего доставляете артефакты к себе в контейнер. Через полчаса ваши пользователи чувствуют себя чуть лучше.

У нас получилось уменьшить вес фронтенда на 30% — получится и у вас! Всем легких сайтов.

Ссылочки:


© Habrahabr.ru