Привет! Я — Ваня, лид платформенной команды в Тинькофф Бизнес.
Мое любимое занятие — открывать вкладку DevTools и проверять, сколько весят артефакты сайта. В этой статье расскажу, как мы сократили вес приложения на 30% силами платформенной фронтенд-команды за один день без изменения кода сайта. Никаких хитростей и регистраций — только nginx, docker и node.js (опционально).
![](https://habrastorage.org/r/w1560/webt/88/xl/fo/88xlfoc-7kpdci8ptgwy-shah2e.png)
Сейчас фронтенд-приложения весят немало. Собранные артефакты могут весить 2—3 Мб, а то и больше. Однако пользователям на помощь приходят алгоритмы сжатия.
До недавнего времени мы использовали только Gzip, который был представлен миру еще в 1992 году. Наверное, это самый популярный алгоритм сжатия в вебе, его поддерживают все браузеры выше IE 6.
Напомню, что уровень сжатия у Gzip изменяется в диапазоне от 1 до 9 (больше — эффективнее), а сжимать можно либо «на лету», либо статически.
Очевидно, что первый вариант требует больше ресурсов сервера на каждый запрос. Второй же — на этапе сборки и подготовки приложения.
Наш фронтенд сжимался динамически четвертым уровнем. Продемонстрирую разницу между сжатым артефактом и исходным:
Можно заметить, что даже четвертый уровень сокращает размер артефакта в 4 раза! А разница между четвертым уровнем и девятым составляет 35 Кб, то есть 1,3% от исходного, но в 2 раза увеличивается время сжатия.
И вот недавно мы задумались: почему бы не перейти на Brotli? Да еще и на самый мощный уровень сжатия!
К слову, этот алгоритм был представлен Google в 2015 году и имеет 11 уровней сжатия. При этом четвертый уровень Brotli эффективнее девятого у Gzip. Я замотивировался и быстро накидал код для сжатия артефактов алгоритмом Brotli. Результаты представлены ниже:
Однако из таблицы видно, что даже первый уровень сжатия Brotli выполняется дольше, чем девятый у Gzip. А последний уровень — аж 8,3 секунды! Это насторожило меня.
С другой стороны, результат однозначно впечатляет. Далее я попробовал перенести сжатие на nginx — загуглил документацию. Все оказалось предельно просто:
Собрал докер-образ, запустил контейнер и был ужасно удивлен:
![](https://habrastorage.org/r/w1560/webt/o5/mk/p_/o5mkp_c6uyhyrye2i7oq3eok5qk.png)
Время загрузки моего файла выросло в десятки раз — со 100 мс до 5 секунд! Приложением стало невозможно пользоваться.
Изучив документацию глубже, понял, что можно раздавать статически. Воспользовался ранее написанным скриптом, сжал те же артефакты, положил в контейнер, запустил. Время загрузки вернулось в норму — победа! Однако радоваться рано, потому что доля браузеров, поддерживающих этот тип сжатия, — около 80%.
Это означает, что необходимо сохранить обратную совместимость, при этом дополнительно хочется использовать самый эффективный уровень Gzip. Так появилась идея сделать утилиту по сжатию файлов, которая позже получила название «Шакал».
![](https://habrastorage.org/r/w780q1/webt/oa/dz/7l/oadz7ljyuu-vouck3fsoafeulis.jpeg)
Nginx, Docker и Node.js, хотя при желании можно и на bash.
С Nginx почти все понятно:
Но что делать с приложениями, которые еще не успели обновить докер-образ? Правильно, добавить обратную совместимость:
Объясню принцип работы:
![](https://habrastorage.org/r/w1560/webt/me/wj/g1/mewjg1g0apbwqca2pki2niud9vi.png)
Клиент при каждом запросе передает заголовок Accept-Encoding, в котором перечисляет через запятую поддерживаемые алгоритмы сжатия. Обычно это deflate, gzip, br.
Если у клиента в строке есть br, то nginx ищет файлы с расширением .br, если таких файлов нет и клиент поддерживает Gzip, то ищет .gz. Если таких файлов нет, то пожмет «на лету» и отдаст с четвертым уровнем компрессии.
Если клиент не поддерживает ни один тип сжатия, то сервер выдаст артефакты в исходном виде.
Однако возникла проблема: наш докер-образ nginx не поддерживает модуль Brotli. За основу я взял готовый докер-образ.
С балансировкой трафика разобрались, но откуда взять артефакты? Вот здесь-то и придет на помощь «Шакал».
Это утилита для сжатия статики вашего приложения.
Сейчас это три node.js-скрипта, обернутые в докер-образ с node:alpine. Пробежимся по скриптам.
base-compressor — скрипт, который реализует базовую логику по сжатию.
Аргументы на вход:
gzip.js — файл с вызовом base-compressor с переданной функцией Gzip из пакета zlib и указанием девятого уровня компрессии.
brotli.js — файл с вызовом base-compressor с переданной функцией Brotli из одноименного npm-пакета и указанием 11-го уровня компрессии.
Разобрались, как он работает, теперь можно смело запускать:
В указанных директориях скрипт рекурсивно сожмет все файлы с указанными расширениями .js, .json, .html, .css и сохранит рядом файлы с расширениями .br и .gz. На нашем проекте этот процесс занимает около двух минут при весе всех артефактов около 6 Мб.
На этом моменте, а может быть, и раньше вы могли подумать: «Какой докер? Какая нода? Почему бы просто не добавить два пакета к себе в package.json проекта и вызывать прямо на postbuild?»
Лично мне очень больно видеть, когда ради прогона линтеров в CI проект устанавливает себе 100+ пакетов, из которых ему на этапе линтинга нужны максимум 10. Это время агента, ваше время, как никак time to market.
В случае с докером мы получаем заранее собранный образ, в котором установлено все необходимое именно для сжатия. Если вам сейчас не нужно ничего сжимать — не сжимайте. Нужен линт — прогоняйте только его, нужны тесты — прогоняйте только их. Плюс мы получаем хорошее версионирование «Шакала»: нам не нужно обновлять его зависимости в каждом проекте — достаточно выпустить новую версию, а проекту — использовать latest-тег.
Помочь своим пользователям можно прямо сейчас, прямо следующим ПРом: добавляете шаг после сборки — сжатие «Шакалом», после чего доставляете артефакты к себе в контейнер. Через полчаса ваши пользователи чувствуют себя чуть лучше.
У нас получилось уменьшить вес фронтенда на 30% — получится и у вас! Всем легких сайтов.
Мое любимое занятие — открывать вкладку DevTools и проверять, сколько весят артефакты сайта. В этой статье расскажу, как мы сократили вес приложения на 30% силами платформенной фронтенд-команды за один день без изменения кода сайта. Никаких хитростей и регистраций — только nginx, docker и node.js (опционально).
![](https://habrastorage.org/webt/88/xl/fo/88xlfoc-7kpdci8ptgwy-shah2e.png)
Зачем
Сейчас фронтенд-приложения весят немало. Собранные артефакты могут весить 2—3 Мб, а то и больше. Однако пользователям на помощь приходят алгоритмы сжатия.
До недавнего времени мы использовали только Gzip, который был представлен миру еще в 1992 году. Наверное, это самый популярный алгоритм сжатия в вебе, его поддерживают все браузеры выше IE 6.
Напомню, что уровень сжатия у Gzip изменяется в диапазоне от 1 до 9 (больше — эффективнее), а сжимать можно либо «на лету», либо статически.
- «На лету» (динамически) — артефакты хранятся в полученном после сборки виде, их сжатие происходит во время выдачи на клиент. В нашем случае на уровне nginx.
- Статически — артефакты после сборки сжимаются, а HTTP-сервер выдает их на клиент «как есть».
Очевидно, что первый вариант требует больше ресурсов сервера на каждый запрос. Второй же — на этапе сборки и подготовки приложения.
Наш фронтенд сжимался динамически четвертым уровнем. Продемонстрирую разницу между сжатым артефактом и исходным:
Уровень сжатия |
Вес артефакта, Кб |
Время сжатия, мс |
---|---|---|
0 |
2522 |
– |
1 |
732 |
42 |
2 |
702 |
44 |
3 |
683 |
48 |
4 |
636 |
55 |
5 |
612 |
65 |
6 |
604 |
77 |
7 |
604 |
80 |
8 |
603 |
104 |
9 |
601 |
102 |
Можно заметить, что даже четвертый уровень сокращает размер артефакта в 4 раза! А разница между четвертым уровнем и девятым составляет 35 Кб, то есть 1,3% от исходного, но в 2 раза увеличивается время сжатия.
И вот недавно мы задумались: почему бы не перейти на Brotli? Да еще и на самый мощный уровень сжатия!
К слову, этот алгоритм был представлен Google в 2015 году и имеет 11 уровней сжатия. При этом четвертый уровень Brotli эффективнее девятого у Gzip. Я замотивировался и быстро накидал код для сжатия артефактов алгоритмом Brotli. Результаты представлены ниже:
Уровень сжатия |
Вес артефакта, Кб |
Время сжатия, мс |
---|---|---|
0 |
2522 |
— |
1 |
662 |
128 |
2 |
612 |
155 |
3 |
601 |
156 |
4 |
574 |
202 |
5 |
526 |
227 |
6 |
512 |
249 |
7 |
501 |
303 |
8 |
496 |
359 |
9 |
492 |
420 |
10 |
452 |
3708 |
11 |
446 |
8257 |
Однако из таблицы видно, что даже первый уровень сжатия Brotli выполняется дольше, чем девятый у Gzip. А последний уровень — аж 8,3 секунды! Это насторожило меня.
С другой стороны, результат однозначно впечатляет. Далее я попробовал перенести сжатие на nginx — загуглил документацию. Все оказалось предельно просто:
brotli on;
brotli_comp_level 11;
brotli_types text/plain text/css application/javascript;
Собрал докер-образ, запустил контейнер и был ужасно удивлен:
![](https://habrastorage.org/webt/o5/mk/p_/o5mkp_c6uyhyrye2i7oq3eok5qk.png)
Время загрузки моего файла выросло в десятки раз — со 100 мс до 5 секунд! Приложением стало невозможно пользоваться.
Изучив документацию глубже, понял, что можно раздавать статически. Воспользовался ранее написанным скриптом, сжал те же артефакты, положил в контейнер, запустил. Время загрузки вернулось в норму — победа! Однако радоваться рано, потому что доля браузеров, поддерживающих этот тип сжатия, — около 80%.
Это означает, что необходимо сохранить обратную совместимость, при этом дополнительно хочется использовать самый эффективный уровень Gzip. Так появилась идея сделать утилиту по сжатию файлов, которая позже получила название «Шакал».
![](https://habrastorage.org/webt/oa/dz/7l/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;
Объясню принцип работы:
![](https://habrastorage.org/webt/me/wj/g1/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 — скрипт, который реализует базовую логику по сжатию.
Аргументы на вход:
- Функция сжатия — любая javascript-функция, можно реализовать свой алгоритм сжатия.
- Параметры сжатия — объект с параметрами, необходимыми для переданной функции.
- Расширение — расширение артефактов сжатия. Необходимо указывать начиная с символа точки.
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% — получится и у вас! Всем легких сайтов.
Ссылочки:
- Докер-образ утилиты
- Репозиторий утилиты
- UPD: Благодаря kellas появилась CLI версия в виде npm пакета