...

четверг, 7 ноября 2019 г.

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

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

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


Зачем


Сейчас фронтенд-приложения весят немало. Собранные артефакты могут весить 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;

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

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

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

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

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


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;

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

Клиент при каждом запросе передает заголовок 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% — получится и у вас! Всем легких сайтов.

Ссылочки:


Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий