Хабрахабр

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

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

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

Зачем

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

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

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

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

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

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

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

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

При этом четвертый уровень Brotli эффективнее девятого у Gzip. К слову, этот алгоритм был представлен Google в 2015 году и имеет 11 уровней сжатия. Результаты представлены ниже:
Я замотивировался и быстро накидал код для сжатия артефактов алгоритмом 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?»

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

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

Результат:

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

Итого

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

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

Ссылочки:

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть