Так уж случилось, что в последнее время мне приходится осваивать новые инструменты. Очередным таким инструментом стал webpack. Инструмент интересный, но после переезда с Google Closure * для меня стало загадкой, почему webpack не ужимает имена классов, как это делает Google Closure Stylesheets. За день, на коленке, мною был написан plugin который вполне не плохо реализовал этот функционал. Более подробное описание ниже.

И так начнем с ТЗ. Это делается, во первых, для себя, во вторых, для тех кто еще не понял, что ту происходит, но каким-то образом попал на эту страницу. Лично я люблю писать большие и красивые длинные имена классов, по которым сразу понятно, что происходит.

Например:

.header {
  position: fixed;
  top: 0;
  ...
}
.header a {
  display: block;
  float: right;
  ...
}
.sidebar {
  float:right;
  max-width: 30%;
  ...
}
.sidebar a {
  font-size: 16px;
  ....
}

Но можно же сократить header до h, a sidebar до s, таким образом сэкономив не мало байт не только в CSS, но и JS файлах, т.к. скорее всего ваш JS будет содержать селекторы по именам классов.

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

Небольшое пояснение как это работает в Closure


Google Closure состоит из нескольких инструментов, один из которых Google Closure Stylesheets, который является и препроцессором и постпроцессором для таблиц стилей.

Как препроцессор он аналогичен всем своим собратьям, но больше всего он похож на SCSS/SASS.
Как постпроцессор он парсит имена классов создавая словарь замен и заменяет все имена классов на их короткие обозначения.

На пример, код выше станет:

.a {
  position: fixed;
  top: 0;
  ...
}
.a a {
  display: block;
  float: right;
  ...
}
.b {
  float:right;
  max-width: 30%;
  ...
}
.b a {
  font-size: 16px;
  ....
}

А словарь замен будет:

{
  "header": "a",
  "sidebar": "b"
}

На самом деле функционала там намного больше, но статья не о том. Есть еще Closure Templates, неплохой шаблонизатор, в котором надо отметить все имена классов специальной директивой, что бы потом применить словарь замен к шаблонам.

Например:

{namespace test}

/**
* Test template
*
*/
{template .test}
<div class="{css header}">Header<a href="#">Home</a><a href="#">About</a><header>
...
<div class="{css sidebar}">Sidebar<header>

Так же не надо забывать, что у нас есть JS в котором тоже надо «доработать» имена всех CSS классов:

var header = goog.dom.getElementByClass(goog.getCssName('header'));
var sidebar = goog.dom.getElementByClass(goog.getCssName('sidebar'));

И только когда мы исправим все исходники и отправим их на компиляцию, вместе со словарем замен, то все заработает.

Главный недостаток этого метода, что словарь собран по CSS, т.е. если у вас есть класс который используется только для выборки DOM элемента из JS, то в словарь он может не попасть (а может и попасть, но оговорюсь, что данная статья не обзор Closure Tools).

Вернемся к плагину


Раскидывать везде функции, мне показалось не очень удобно, по этому я решил задавать имена классов по маске ___<%className%>__.

Таким образом стили придут к виду:

.___header__ {
  position: fixed;
  top: 0;
  ...
}
.___header__ a {
  display: block;
  float: right;
  ...
}
.___sidebar__ {
  float:right;
  max-width: 30%;
  ...
}
.___sidebar__ a {
  font-size: 16px;
  ....
}

А работа с DOM в JS, на примере jQuery:

  var header = $('.___header__');
  var sidebar = $('.___sidebar__');

На примере React:

function Header(props) {
  return (
    <div className="___header__">
      {props.children}
    </div>
  );
}

На примере Backbone:

module.exports = Backbone.View.extend({
  tagName: 'div',
  className: '___header__'
});


UPD:
Для Angular пример получился толстым.

Оговорюсь сразу, что конструкции типа:

var genClassName = function(v) {
  return '___' + v + '__';
}
module.exports = Backbone.View.extend({
  tagName: 'div',
  className: genClassName('header')
});

работать не будут. Также как и стили:

[class*="bold"] {
  font-weight:bolder;
}

Первые шаги


Установив пакет:

npm install --save cssrename-webpack-plugin

И немного доработав webpack.config.js:

const CssRenameWebpackPlugin = require('cssrename-webpack-plugin');
...
module.exports = {
  ...
  plugins: [
    CssRenameWebpackPluginConfig,
     ...
  ]
};

В процессе сборки появится строчка:

«Profit: 355»

Которая будет сообщать о том сколько байт было сэкономлено.

Неудобства и их решения


Но если в животном разнообразном мире JS существует огромное количество библиотек, которые одним парсером не покрыть, то вот CSS в этом вопросе значительно гуманнее, и распарсить его (CSS) значительно проще.

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

npm install --save cssrename-loader

Очередные мутации webpack.config.js:

module.exports = {
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: "style-loader!css-loader!cssrename-loader"
      }
    ]
  }
};

То что получилось с тестовым проектом

Update:

Писать я хотел не об этом, но видимо надо добавить. Далее рассмотрены только стили, выигрыш по JS и шаблонам так просто не посчитать. Для Google Closure Stylesheets:
Сайт/Файл Оригинальный объем/zip Полученный объем/zip Процент экономии(zip)
acss.io bundle.488facb7.css 13.8KB/4.4KB 13.3KB/4.0KB ?9%
getbem.com/introduction getbem.com.1.0.0.css 13.3KB/3.3KB 12KB/3.1KB ?6%
Bootstrap 121.2KB/18.7KB 96.9KB/16.7KB ?10%
habrahabr.ru global_main.css 212.3KB/30.6KB 155.2KB/26.9KB ?13%

Пруф

Что «не пролезло» в Google Closure Stylesheets:

Для Atomic пришлось сделать две замены по регулярным выражениям:

\\\(((?:(?!\\\)).)*?)\\\) => --$1--
\\. => --

Для BEM «не пролезло»:

@supports (display: -moz-box) {
    [class*=LineClamp] {
        display: block
    }
}

    @-webkit-keyframes bounce {
        0% {
            -webkit-transform: translateY(-100%);
            transform: translateY(-100%);
            -webkit-filter: blur(5px);
            filter: blur(5px)
        }
        100%, 40% {
            -webkit-transform: translateY(0);
            transform: translateY(0)
        }
        60% {
            -webkit-transform: translateY(-10%);
            transform: translateY(-10%)
        }
    }
@keyframes bounce {
         0% {
             -webkit-transform: translateY(-100%);
             transform: translateY(-100%);
             -webkit-filter: blur(5px);
             filter: blur(5px)
         }
         100%, 40% {
             -webkit-transform: translateY(0);
             transform: translateY(0)
         }
         60% {
             -webkit-transform: translateY(-10%);
             transform: translateY(-10%)
         }
     }

Из Bootstrap «не пролезло»:

border-top: 4px solid \9;
@media all and (transform-3d),(-webkit-transform-3d)
@-ms-viewport {
    width: device-width
}

Из Хабра «не пролезло»:

@charset "UTF-8";

@-moz-document url-prefix() {
    .search-field__select {
        text-indent: .01px;
        text-overflow: ''
    }
}
Поделиться с друзьями
-->

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


  1. kafeman
    29.11.2016 21:09
    +5

    почему webpack не ужимает имена классов
    Потому что не имеет смысла. У Гугла, мне кажется, так просто исторически сложилось.


    1. edejin
      29.11.2016 21:35

      Не скажите.
      Я тут прошелся Google Closure Stylesheets по
      https://habracdn.net/habr/styles/1480427855/_build/global_main.css
      И получил 155KB против 207KB и это только стили без скриптов и объемов респонса, т.к. хабр не SPA.
      А вот для SPA это был бы отличный профит.


      1. kafeman
        29.11.2016 21:50
        +1

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


        1. edejin
          29.11.2016 21:54

          Вы правы, не спорю.
          С зипом профит только по CSS 5KB.


        1. edejin
          29.11.2016 22:03

          И 3KB (в зипе) на HTML конкретно этой страницы.
          Сколько там у хабра заходов в день…


          1. vintage
            29.11.2016 22:11
            +5

            Что капля в море, что стакан в океане — погоды они не делают.


          1. kafeman
            29.11.2016 22:12
            +2

            Стоит ли париться из-за этих 8 Кб, 5 из которых будут закешированны? Всякие картинки весят значительно больше, вот с них и стоит начать оптимизизацию.

            Хотел провести эксперимент сам, но closure-compiler ругается на 79 ошибок Хабра. Вы, вероятно, его с какими-то особыми флагами запускаете, чтобы успокоить?


            1. edejin
              29.11.2016 22:15

              Как-то так:
              java -jar ~/Загрузки/closure-stylesheets.jar --output-renaming-map-format CLOSURE_UNCOMPILED --rename CLOSURE --allow-unrecognized-properties --allow-unrecognized-functions --output-renaming-map renaming_map.js ~/Загрузки/global_main.css -o ./out.css

              Советую отпритепринтить.

              И вот это он точно не схавает.
              charset removed in .../global_main.css at line 1 column 1:
              charset «UTF-8»;
              ^

              unknown @ rule in .../global_main.css at line 2 378 column 1:
              @-moz-document url-prefix() {
              ^

              GSS constant not defined: MS in .../global_main.css at line 8 314 column 28:
              font-family: Trebuchet MS
              ^

              3 error(s), 0 warning(s)


  1. lexich
    30.11.2016 00:04
    +1

    Ну можно использовать css-modules и правильно настроить генерацию имен классов, но как уже обсудили выше — это неуловимый Джо.


  1. Akuma
    30.11.2016 00:58
    +3

    Для проекта с посещаемостью меньше Google, Facebook и аналогичных, это бессмысленная трата времени и сил разработчика, а так же большие шансы напороться на ошибку в JS.

    Когда-то уже обсуждалось, что гугл таким образом может существенно экономить трафик, т.к. 1 Кб экономии при миллиарде просмотров экономит почти 1 Тб трафика, что довольно неплохо. Но вот «обычному» проекту с посещаемостью пусть даже 100000 в сутки — это экономия на спичках.


  1. imater
    30.11.2016 02:31

    Читаемость важнее. Вспоминайте это при строительстве велосипедов.


  1. 3axap4eHko
    30.11.2016 03:37

    а как вы решаете вопрос при манипуляции классами из js? Например я через classList.toggle переключаю класс или classList.add добавляю его. учитывает ли это оптимизатор?


    1. edejin
      30.11.2016 03:53

      ну например у вас было:

      elem.classList.toggle("you");
      

      А теперь вы должны написать:
      elem.classList.toggle("___you__");
      

      А в результате получилось:
      elem.classList.toggle("a");
      

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

      Если я конечно, с просони, правильно понял ваш вопрос.


  1. jbubsk
    30.11.2016 08:51

    Боюсь представить, что бы мне ответила команда, если бы я предложил им: «коллеги, а может попробуем это?»


  1. ruzhovt
    30.11.2016 09:49
    +1

    gzip + кеш браузера решают 99% проблем с трафиком.

    webpack создавался не для экономии трафика ;)

    а идея с переименованиями классов — вообще мрак.


    1. knotri
      30.11.2016 12:20

      Да почему мрак? Если пользоватся БЕМ-ом — там огромные именна классов (пусть и которые хорошо жмутся gzip).


      1. ruzhovt
        30.11.2016 20:30

        потому что поломается половина javascript кода. имена классов часто используются для ссылания на конкретный дом элемент из кода. и да спасибо за минус в карму, вам того же и вдвойне :)


        1. edejin
          30.11.2016 20:47

          Простите, а можно пример поломки javascript? По возможности кусок сорца.


          1. ruzhovt
            01.12.2016 09:39

            <a href="#" ng-class="{selected: ctx.counter > 5}"/>
            

            вот.


            1. edejin
              01.12.2016 15:16

              А у меня для вашего примера получилось.
              Вот так:

              <a href="#" ng-class="{___selected__: ctx.counter > 5}"/>
              

              Я три часа как начал читать, что такое Angular, и видимо что-то делаю не правильно…
              Добавил в сталью пример для Angular.


  1. vtrushin
    08.12.2016 14:10
    -1

    Вы предлагаете обернуть, почему-то несбалансированным количеством андерскоров (слева 3, справа 2), все классы только для того, чтоб сэкономить копейки кода? Взамен получаете трудночитаемые стили (учитывая еще, что в бэме тоже используется __ и _) и очень большую вероятность ошибки в количестве андерскоров. Ну и конечно же трудночитаемость коснется и хтмла