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

Автор оригинала: 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)

Комментарии 13

    +1
    Можно взять 52 битное кодирование ([a-zA-Z]+) или 64-битное ([a-zA-Z0–9_-]+)

    Биты немного не так работают.
    Base64 != "64-битное кодирование". А "шестнадцатеричное число" — не то же самое, что "16-битное число".

      0
      Согласен, терминологию можно уточнить.
      +1
      А зачем так заморачиваться с хешами?
      Не проще ли сделать последовательное переименование классов с добавлением новой буквы, когда алфавит кончается?
      a, b, c, ... , y, z, aa, ab, ac, ... , ay, az, ba, bb, bc, ...
      Экономия букв налицо.
        0
        В кодировке уже есть 1 байт под символ, поэтому мы берем всё что допустимо [a-zA-Z0–9_-] 64 значения. У вас в примере 26. А хеши нужны во избежание коллизий, потому что в css-modules глобальная видимость имён.
          +1
          А хеши нужны во избежание коллизий, потому что в css-modules глобальная видимость имён.

          Всё ещё не видно проблемы. Глобальный счётчик при сборке полностью решает проблему. Это вот как раз хеши имеют коллизии (что впрочем тоже можно решить сверившись с теми хешами, которые уже созданы, дабы не дублировать).

            0
            Глобальный счетчик порождает другие проблемы — рассинхрон сборки бека/фронта, нужен лишний проход сборки (или отдельный плагин), и разбивание долгосрочных кешей для всего проекта
              0
              или отдельный плагин

              Там уже есть возможность указать метод для формирования имени. Но в целом соглашусь, что минусов хватает.

                0
                формирование идёт ассинхронно. чтобы засинхронить кеши (и бек с фронтов) нужен ещё проход для сбора всей инфы по файлам, и их сортировка
                  0

                  Ну тут либо "по-взрослому" с кучей геморроя, с сомнительным выхлопом, либо по-простому, в пару строк, но последствиями, о которых вы выше написали.

            0
            Насчет байтов понятно. Мой пример можно расширить под 64 значение: модифицировать так, чтобы закончился не только алфавит, но и A-Z0–9_-. А потом начать сначала по тому же принципу.
            Но я, в данном случае, все равно, не понимаю назначения хеша. Ведь мы избегаем коллизий добавляя ещё один символ к строке.
              +2
              префикс работает на уровне файла. на уровне проекта работает хеш по пути файла. чтобы не использовать хеш — нужно собирать файлы до начала сборки, сортировать их, и потом по списку работать. тогда будет глобальный нейминг и хеши не нужны. но первое же изменение любого файла сломает кеширование, и будет полностью новый билд. если говорить про большие проекты, то это недопустимо. текущее решение — компромисс перехода по кешу с уровня класса на уровень файла.
          0
          Думаю что такие бандлы будут хуже сжиматься gzip и итоговой результат (сколько передали в сжатом виде по сети) будет не особо лучше.
            0
            сжимается гораздо лучше, т.е. рандомый хеш != одинаковые строки. попробуйте

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое