Pull to refresh

Как уменьшить размер бандла — стратегия однобуквенных классов в css-modules

Reading time 2 min
Views 3.3K
Original author: denisx
Улучшаем компрессию бандлов на 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 Попробовать на своём проекте можно уже сейчас, если взять код здесь

P.S.3 В продакшене сжимаем на 93% файлы *.css и *style.js. Передаём 71,6Kb от 1,1Mb распакованного файла (brotli)
Tags:
Hubs:
+9
Comments 13
Comments Comments 13

Articles