[Перевод] Как уменьшить размер бандла — стратегия однобуквенных классов в css-modules
Улучшаем компрессию бандлов на 40% от размера файла, путём замены стандартного хеширования на однобуквенный префикс + хеш пути файла.
Css-modules позволяют написать компоненты Bird и Cat, со стилями в файлах с одинаковым именем styles.css и классами .block в каждом, и эти классы будут разные для каждого из этих компонентов.
/* Bird / styles.css */
.block { }
.name { }
/* Cat / styles.css */
.block { }
.name { }
Ничего хитрого тут нет: вебпак хеширует каждый класс из всех файлов с помощью настройки »[hash: base64:8]». Все классы будут переименованы, и проставлены ссылки, чтобы понимать, какой класс откуда взяли. В базовом варианте сборки, у нас будет файл styles.css для стилей и styles.js для ссылок при работе с js.
Продолжая тестовый пример, получаем 4 независимых класса со странными именами типа k3bvEft8:
/* Bird */
.k3bvEft8 { }
.f2tp3lA9 { }
/* Cat */
.epIUQ_6W { }
.oRzvA1Gb { }
Запустим продакшн-сборку и сожмём файлы. На рабочем стенде, 300Kb css-файл стал упакован в 70Kb с помощью gzip [или 50Kb с помощью brotli]. Сжатие небольшое, потому что хеши — случайные сгенерированные строки, очень плохо сжимаются. Алгоритмы сжатия не видят последовательностей и вынуждены запоминать местоположения каждого символа, т.е. передавать содержимое этих участков как есть, без сжатия.
Что-то надо с этим делать. Но что? Во время работы, вебпак считывает дерево файлов асинхронно, и также проходит по названиям классов. Каждый раз по-разному. Единственное, за что можно зацепиться — это порядок имён внутри css — он постоянен (иначе всё сломается, в css порядок важен). Номер позиции класса в файле закодируем в однобуквенный префикс. Можно взять 52 битное кодирование ([a-zA-Z]+) или 64-битное ([a-zA-Z0–9_-]+) Тут главное не забыть проставить защитный префикс в случаях с цифрой или дефисом.
/* Bird */
.a { }
.b { }
/* Cat */
.c { }
.d { }
Вроде выглядит неплохо — имена сжались максимально. Но загвоздка в том, что вебпак асинхронный, и каждый запуск, и особенно при параллельном запуске серверной и клиентской одновременной сборки, получает файлы в хаотичном порядке, как и имена классов. Спасибо за скорость, но тут это мешает.
/* Bird */
.c { }
.d { }
/* Cat */
.a { }
.b { }
Видите, поймали несовпадение порядка файлов.
Пофиксим это поведение, запомнив файл, откуда пришли классы, и номер их позиции.
/* Bird */
.a { }
.b { }
/* Cat */
.a { }
.b { }
Сохранили порядок внутри файлов. Но нужно как-то отличать файлы друг от друга. Избежать путанницы поможет хеш от пути файла.
/* Bird */
.a_k3bvEft8 { }
.b_k3bvEft8 { }
/* Cat */
.a_oRzvA1Gb { }
.b_oRzvA1Gb { }
('_' здесь не нужен, он только для наглядности. Хеш имеет стабильную длинну, в отличие от префикса, и здесь не может быть коллизий)
У нас получились абсолютно уникальные для проекта имена классов, но содержащие повторяющиеся поледовательности.
В нашем проекте, из файлов css 50 Kb и js 47 Kb получили css 30 Kb и js 28 Kb [58 Kb суммарно, brotli].
Экономия почти 40Kb. Немного уменьшится и размер критичного css, и размер html.
Осталось написать класс для обработки данных из вебпака и прокинуть вызов в конфиге css-loader (getLocalIdent)
P.S. Можно пойти дальше и сохранять пути файлов, сортировать пути, и тоже заменять по однобуквенной стратегии, но это хуже в плане долгосрочного кеширования, плюс нужно делать несколько проходов в сборке и собирать клиент/сервер последовательно.
P.S.2 Попробовать на своём проекте можно уже сейчас, если взять код из пр github.com/webpack-contrib/css-loader/pull/1062
P.S.3 В продакшене сжимаем на 93% файлы *.css и *style.js. Передаём 71,6Kb от 1,1Mb распакованного файла (brotli)