Долгие годы шли споры, как лучше именовать классы - по бэму, по целям, по компонентам или как угодно, но с добавлением хеша. И это действительно важный вопрос, какой способ будет комфортен в разработке большого и развивающегося проекта. Но, что эти способы означают для пользователя, нужны ли ему эти классы и как они связанны с его опытом?

Порою, заходя в стили проектов, я невольно пугаюсь сложившейся длине имён - модуль, блок, элемент, подэлемент, модификатор 1, модификатор 2. БЭМ действительно великолепен и я не собираюсь его отрицать, но его размеры оставляют желать лучшего.

Длинные классы увеличивают вес страницы, это в свою очередь означает увеличение времени загрузки самого главного для рендера страницы - документа и файла стилей, от которых напрямую зависят метрики FCP, LCP.

Это стало одной из причин почему я долгое время засматриваюсь на модули (в дополнение к изоляции стилей и их хранении там, где они используется).

Модули дают возможность называть классы короче, только под текущий компонент, сохраняя при этом удобство разработки. Но теперь к классам добавляются хеши, делая их длиннее, а значит преимущество не такое, как хотелось бы. Поэтому, наконец, к теме статьи.

Сжатие имён классов

Итак, какие есть методы сократить классы:

  1. Называть классы короче (Спасибо, Кэп, да);

  2. Называть классы полноценно, но при сборке оставлять только хеш;

  3. Называть имена по определённому правилу или алгоритму.

Первый способ не подходит для чего-то большего, чем to-do лист - сделав классы слишком короткими мы либо теряем DX, либо рискуем сделать пересечения.

Для второго и третьего же способа, css-loader предлагает свойство localIdentName для модулей.

localIdentName: "[path][name]__[local]--[hash:base64:5]"

localIdentName: "[hash:base64]"

Самое оптимальное сжатие

Подобрав правило можно значительно сократить размер классов, но всё же, значительно не равно максимально. Максимальным уменьшением имён классов будет сохращение до символов - .a, .b, .c, …, etc.

Таким подходом пользуются, например, Google, Facebook, Instagram

Чтобы реализовать такое решение нас интересует свойство getLocalIdent, которое позволяет передавать функцию для генерирования имени. Также можно использовать такие пакеты как posthtml-minify-classnames или mangle-css-class-webpack-plugin.

На этом можно было бы завершать статью, если бы не одна деталь. Я использую next.js.

Решение

Next.js имеет несколько особенностей, которые не позволяют использовать эти решения. Самой очевидной особенностью является то, что он не даёт возможности настроить getLocalIdent снаружи.

Именно поэтому я, 3 года назад сделал пакет - next-classnames-minifier. В нём я реализовал алгоритм подбора имён и настроил встраивание getLocalIdent в нужные правила в вебпаке. За последующие годы пакет незначительно обновлялся, но было в нём то, что не позволяло мне назвать его завершённым и готовым для использования в коммерческих проектах.

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

Если компонент собрался, то при следующем запуске режима разработки или сборки - он может не пересобираться. То есть при перезапуске, алгоритм начинал работать с чистого листа и генерировал классы с самых простых имён (.a, .b, .c), но в ряде непересобираемых компонент и стилей такие имена были добавлены при прошлом запуске.

По этой причине решение и не включено в next.js

Ответ со-автора next.js на сжатие имён классов.
Ответ со-автора next.js на сжатие имён классов.

Подружиться с Next.js

Очевидно, критично важной задачей было избавить от проблемы очистки кеша. И решение нашлось. Теперь пакет, подобно next.js, кеширует результаты - сгенерированные имена - и при каждом запуске восстанавливает их из кеша, анализируя их и проверяя их актуальность.

При этом, скорость сборки не стала дольше, ведь пакет использует всё тотже оптимизированный алгоритм подбора имени, а за счёт кеширования пакет работает ещё быстрее [чем базовое создание имён с генерированием хеша].

Эффективность

Можно найти статьи с эффективностью сжатия 30%, 50% и даже 70%. В реальности же всё очень индивидуально. Например, если у вас был класс:

.postsListItemTitle {
	font-size: 24px;
}

Из него получится:

.j1 {
	font-size: 24px;
}

21 символ (.j1{font-size: 24px;}) вместо 44 (.postsListItemTitle__h53_j{font-size: 24px;}) - экономия 52%. Этот класс используется в 20 карточках на странице, что уменьшает вес и html.

В среднем же, пожалуй, можно говорить о уменьшении веса css на 10-20%.

next-classnames-minifier - давайте сделаем веб Ещё быстрее.

Комментарии (23)


  1. fire_engel
    16.01.2024 10:54
    +3

    Длинные классы увеличивают вес страницы, это в свою очередь означает увеличение времени загрузки самого главного для рендера страницы - документа и файла стилей

    ну да, виноваты классы, а не фреймворки, генерирующие титаническое количество js и виртуальное дом-дерево. просто не используйте препроцессоры и всё, можно и без них добиться того же функционала.


    1. Vordgi Автор
      16.01.2024 10:54
      +1

      Провожу много времени в попытках оптимизации сайтов. Пробовал svelte, шаблонизаторы, статику, и... они не дали явных преимуществ (особенно по сравнению с next.js v14 и серверными компонентами).

      Всё это - виртуальный дом, множество логики, доп. библиотеки, в контексте next.js не влияют на FCP и LCP, так как Next в первую очередь выдаёт статику, и уже потом на клиенте, грубо говоря, виртуальный дом свяжется с настоящим дом-ом, а это произойдёт уже после подсчёта FCP и LCP или параллельно.

      Увы, это может (может != обязательно будет) влиять на TBT - total blocking time, которая сейчас становится в общем-то основной метрикой для гугла.


      Собственно, раз он сразу выдаёт документ со статикой - то именно от размера самого этого документа и будет зависеть FCP, а не от дополнительных скриптов. Вдвойне это чувствуется, когда классы вшивают в документ.

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


      1. vmkazakoff
        16.01.2024 10:54
        -1

        Если вообще не брать фреймворк, а писать просто классы к элементам, то они, о чудо, отлично применятся браузером. Проблема когда это, CSS in JS и оно тянет за собой ещё десяток node module и несколько мегабайт трэша в браузер. Но писать простой CSS уже разучились (

        Веб переусложнили. И если для каких-то сложных приложений с огромным функционалом это ещё полезно (отдельный вопрос полезно ли их делать в браузере), то когда фреймворки и сотню модулей пихают в любой сайт - это жесть.


        1. Vordgi Автор
          16.01.2024 10:54
          +1

          CSS in JS ужасен, да, поэтому речь только о CSS-модулях. Сам Next.js тоже перестал рекомендовать css-in-js решения и сейчас везде говорят только про css/scss/sass и css-модули (ну ещё бывает tailwind, но с кем не бывает).

          А сайты бывают и сложнее приложений, современным компаниям для продвижения и анализа нужно куда больше, чем просто текст и красивая анимация.


  1. DarthVictor
    16.01.2024 10:54

    В среднем же, пожалуй, можно говорить о уменьшении веса css на 10-20%.

    До сжатия gzip/brotli или после?


    1. Vordgi Автор
      16.01.2024 10:54

      И до, и после (но в меньшем соотношении, да). Видел примеры, где эффект в сжатом виде стремительно приближался к нулю.

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

      Если есть безобидный способ, пусть даже незначительно, сократить вес страницы, то почему бы и да.


  1. dom1n1k
    16.01.2024 10:54

    А что значит "именование классов по целям"? Гугл первой ссылкой выдает вот эту самую статью.


    1. Vordgi Автор
      16.01.2024 10:54

      Да, плохая формулировка, спасибо!

      Имелось ввиду форматы, когда классы добавляются не по полноценному БЭМ-у (в котором есть сам класс и его модификаторы), а просто общие классы, то есть когда может быть:
      <div class="card section box pt-20 mb-8" />
      Это bootstrap и tailwind, например. Но если bootstrap устроен всё-же по БЭМ-у, то tailwind просто огромный набор атомарок, который у меня язык не поворачивается назвать БЭМ-ом (и выглядит это порою очень страшно).
      Кто-нибудь, объясните мне, как это вообще выжило...


      1. finkrer
        16.01.2024 10:54

        Выжило исключительно потому, что так удобнее)


        1. Vordgi Автор
          16.01.2024 10:54
          +1

          Удобство ломается с диким грохотов об блоки чуть сложнее карточки с закруглениями.

          Когда для инпута нужно писать 10 классов на стили, по 5 классов на каждое состояние (hover, visites, disabled, ..., error), 15 классов на адаптив и ещё на всякий случай снаружи принимаешь классы, а то мало ли чего, ещё 20 классов нужно будет передать...


          1. finkrer
            16.01.2024 10:54

            Эта гипотетическая ситуация меня бы запугала, если бы я ее не встречал в работе) Хотите верьте, хотите нет, но на практике это нормально воспринимается. Выглядит — да, страшно.

            If you can suppress the urge to retch long enough to give it a chance, I really think you'll wonder how you ever worked with CSS any other way.

            Это автор фреймворка. И я с ним согласен. Почему это удобнее — может как-нибудь статью напишу про это, там много причин, но проще всего попробовать сделать проект на теилвинде и прочувствовать.


            1. Vordgi Автор
              16.01.2024 10:54

              Я делал относительно нормальный пет проект на нём. И первое время как-то нравилось - буквально plug-n-play - бери и делай, но вот с инпутом, кнопками и прочим с большим количеством условий (когда для элемента в css было бы 40 строк, а здесь это 40 классов) - как-то прям оттолкнуло.


  1. DmitryKazakov8
    16.01.2024 10:54
    +3

    На мой взгляд, сокращение имен классов - это экономия на спичках в ущерб DX.

    1. При сжатии gzip/brotli экономия на большой html будет в байтах, максимум - в паре килобайт. Это не повлияет на оценку Lighthouse и на пользовательский опыт, если вы не гонитесь за тысячными долями процента перфоманса

    2. При чтении дерева элементов в инспекторе видим несемантичные j1 g83 классы, соответственно как найти место в коде, в котором нужно поправить стиль? Можно переключиться на React Dev Tools, выбрать элемент, потом обратно в инспектор, проверить там новые стили, и дальше - копать код проекта, перебирая папки и файлы в рандомном порядке. Проблема усугубляется тем больше, чем больше разработчиков в компании, и чем хуже знаешь кодовую базу проекта. Частично проблема решится CSS maps, но тоже много лишних телодвижений.

      При этом семантичные CSS Modules сразу покажут где находится файл - src-components-layouts-userMenu src-pages-user.

    3. При регистрации ошибки в Sentry например, разработчик смотрит путь, который пользователь накликал (в Сентри это логируется). И в случае сокращенных имен он увидит click[.j1] - как с этим работать? Скорость дебага значительно увеличится.

    4. Для автотестов стабильные имена [path][name]__[local]--[hash:base64:5] можно использовать как уникальные идентификаторы элементов в ряде случаев и обойтись без data-test-id или id (если не учитывать в селекторе hash). Также можно дать не сильно разбирающемуся в автотестах сотруднику визуальный инструмент накликивания пользовательских сценариев. Тогда он передаст разработчику семантичный список действий click[.src-components-layouts-userMenu] и разработчик при необходимости очень быстро добавит id либо оставит селекторы в оригинальном виде, если эти классы достаточно специфичны.

    Итого - сокращением названий классов мы улучшили пользовательский опыт на неизмеримо малую долю, при этом кардинально усложнили работу верстальщикам, разработчикам, тестировщикам и в целом тратим больше денег компании каждый день.


    1. Vordgi Автор
      16.01.2024 10:54
      +1

      При сжатии gzip/brotli экономия на большой html будет в байтах, максимум - в паре килобайт. Это не повлияет на оценку Lighthouse и на пользовательский опыт, если вы не гонитесь за тысячными долями процента перфоманса

      Да, при весе в 100кб сжатыми уменьшит на 5кб (если стили вшиты, например, а может и до 10кб, а может и 1 с трудом накапает, зависит). Это не меняет кардинально метрики, но, например, освобождается 5мб под прочее, может спрайт добавить внутрь? Да и 5кб + доли секунды в упрощении компрессии-декомпрессии сыграют в 1-2 балла (мы всё-таки ориентируемся на мобильные устройства).

      При чтении дерева элементов в инспекторе видим несемантичные j1 g83 классы, соответственно как найти место в коде, в котором нужно поправить стиль? "

      А зачем их читать? В режиме разработки можно настраивать генерирование классов по другому (пакет это поддерживает из коробки). В продакшене ты залезаешь обычно просто конкретный элемент подправить, скорее ради интереса.

      Частично проблема решится CSS maps, но тоже много лишних телодвижений.

      С картами и сентри очень хороший пункт, Спасибо! Вдруг тут кто-то обходил подобное и может поделиться решением?

      А для автотестов классы больно и ненадёжно, атрибут всё же несравним. Мне больше всего нравится подход, когда этот атрибут добавляется во время сборки только в одном окружении, не в проде. Но такое не всем годится. Для накликивания можно было бы тоже сжимать классы только в проде, а все эти махинации проводить на тестовых стендах...


      Стоит ли оно того, если есть другие минусы? Пожалуй нет;
      Стоит ли это попробовать? Почему бы и да. 1 день на аб тесте или ручное сравнение на тестовом стенде даст ответы, есть ли достаточный эффект.

      P.S. Как не плюсануть коммент выше за хороший развёрнутый коммент?


      1. Ilusha
        16.01.2024 10:54

        Сентри умеет в is-сорсмапы, но не знаю, умеет ли в css-сорсмапы. Но в реплеях должно.


      1. gmtd
        16.01.2024 10:54

        100kb стилей? Вы серьезно?


        1. Vordgi Автор
          16.01.2024 10:54

          Бывает и такое, конечно, но имелось ввиду html документ со вшитыми стилями (т.е. когда стили не в отдельных файлах, а в теге <style />, в самой странице)

          (контекст был про "экономия на большой html")


          1. gmtd
            16.01.2024 10:54

            Если вы делаете сайты куда пользователь заходит не больше одного раза, то тогда в ssr есть смысл. Если же вы делаете нормальные сайты, куда пользователь заходит более чем один раз, то pwa с кэшированием сервис worker-om решает все проблемы с большими CSS и не только CSS не уродуя DX.


            1. Vordgi Автор
              16.01.2024 10:54

              Про SSR речи и не идёт - да и там другие проблемы бы были важнее, вроде долгого ожидания начала загрузки документа, что при хорошем интернете обычно даже дольше, чем сама загрузка.

              А сайты продумываются для всех - многие заходят на страницу лишь единожды. А первый вход важен для маркетинговых задач. Если пользователь первый раз грузил страницу 5 секунд, устал и ушёл, то уже неважно, что данные у него закешировались и дальше всё будет грузиться быстро.
              (Когда речь идёт об оптимизациях таких мелочей, обычно это не про веб-приложения, а про маркетинговые сайты)


              1. Vordgi Автор
                16.01.2024 10:54

                Facebook, вероятно, это сделал, чтоб трафик сэкономить, т.к. экономия 2кб со страницы на миллионы (или миллиарды?) пользователей и переходов - весомая экономия на серверах.

                Вряд ли там вообще упоминались улучшение метрик и SEO-оптимизация.


              1. gmtd
                16.01.2024 10:54

                У вас next.js и не ssr? А что у вас тогда?

                И зачем врать про SPA в 5 секунд супротив вашего на полсекунды?


                1. Vordgi Автор
                  16.01.2024 10:54

                  Next.js уже лет 5 как не только про SSR, сейчас в первую очередь это про SSG с инкрементальной пересборкой.

                  А про 5 секунд действительно приукрасил, да. Знаете за сколько загружается эта страница на медленном 3g (что всё ещё является обыденным в мире и Европе)? 20 секунд, из которых 15 до загрузки LCP.

                  Быстрый 3g грузит страницу 5-10 секунд.

                  Остальной мир это не про гигабит в секунду.


  1. Dartess
    16.01.2024 10:54

    Как раз сейчас занимаюсь оптимизацией проекта, идеи что можно ужать подиссякли, попробую этот пакет.