Оптимизация кода CSS с помощью сокращения имен классов и зоны видимости

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

Я реализовал предварительный рендеринг HTML, используя úsus. Этот веб-сервис рендерит HTML одностраничных приложений (SPA) и извлекает критический CSS, добавляя его в HTML. Но для меня неприемлемо встраивание 70 Kb в каждый HTML-документ.

Учимся у Google

Вы когда-либо заглядывали в исходный код https://www.google.com/? Первое, что бросается в глаза – это короткие имена CSS классов.

Содержание

Недостатки CSS минификаторов

Ни один минификатор не может изменять имена селектора. Это связано с тем, что подобные сервисы не контролирует вывод в HTML. Тем временем, имена CSS могут становиться все длиннее.

Если вы используете CSS-модули, то их названия будут включать в себя имя файла таблицы стилей, локальный идентификатор и случайный хеш. Шаблон имени класса описан с помощью конфигурации css-loader localIdentName. Например, [name]___[local]___[hash:base64:5]. Поэтому сгенерированное имя класса будет выглядеть примерно так: .MovieView___movie-title___yvKVV.

Переименовываем имена классов CSS во время компиляции

С помощью webpack и babel-plugin-react-css-modules можно переименовать имена классов во время компиляции, используя конфигурацию CSS Loader getLocalIdent и эквивалентную конфигурацию babel-plugin-react-css-modules generateScopedName.

const generateScopedName = (
   localName: string,
   resourcePath: string
) => {
   const componentName = resourcePath.split('/').slice(-2, -1);
return componentName + '_' + localName; 
};

В generateScopedName один и тот же экземпляр функции можно использовать для процесса сборки и babel, и webpack:

/**
 * @file настройка Webpack.
 */
const path = require('path');

const generateScopedName = (localName, resourcePath) => {
  const componentName = resourcePath.split('/').slice(-2, -1);

  return componentName + '_' + localName;
};

module.exports = {
  module: {
    rules: [
     {
        include: path.resolve(__dirname, '../app'),
        loader: 'babel-loader',
        options: {
          babelrc: false,
          extends: path.resolve(__dirname, '../app/webpack.production.babelrc'),
          plugins: [
            [
              'react-css-modules',
              {
                context: common.context,
                filetypes: {
                  '.scss': {
                    syntax: 'postcss-scss'
                  }
                },
                generateScopedName,
                webpackHotModuleReloading: false
              }
            ]
          ]
        },
        test: /.js$/
      },
      {
        test: /.scss$/,
        use: [
          {
            loader: 'css-loader',
            options: {
              camelCase: true,
              getLocalIdent: (context, localIdentName, localName) => {
                return generateScopedName(localName, context.resourcePath);
              },
              importLoaders: 1,
              minimize: true,
              modules: true
            }
          },
          'resolve-url-loader'
        ]
      }
    ]
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, './.dist'),
    publicPath: '/static/'
  },
  stats: 'minimal'
};

Сокращаем имена

Babel-plugin-react-css-modules и CSS Loader одинаково генерируют имена классов CSS. Поэтому можно менять их как угодно. Тем не менее, важно получить максимально короткие имена классов.

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

const incstr = require('incstr');

const createUniqueIdGenerator = () => {
  const index = {};
  const generateNextId = incstr.idGenerator({
    // Букву d убираем, чтобы исключить сочетание ad, иначе его может заблокировать Adblock.
    // @смотри https://medium.com/@mbrevda/just-make-sure-ad-isnt-being-used-as-a-class-name-prefix-or-you-might-suffer-the-wrath-of-the-558d65502793
    alphabet: 'abcefghijklmnopqrstuvwxyz0123456789'
  });

  return (name) => {
    if (index[name]) {
      return index[name];
    }

    let nextId;

    do {
      // Имя класса не может начинаться с цифры.
      nextId = generateNextId();
    } while (/^[0-9]/.test(nextId));

    index[name] = generateNextId();

    return index[name];
  };
};

const uniqueIdGenerator = createUniqueIdGenerator();

const generateScopedName = (localName, resourcePath) => {
const componentName = resourcePath.split('/').slice(-2, -1);

return uniqueIdGenerator(componentName) + '_' + uniqueIdGenerator(localName);
};

Этот способ гарантирует короткие и уникальные имена классов. И вместо .MovieView___movie-title___yvKVV и .MovieView___movie-description-with-summary-paragraph___yvKVV имена классов превращаются в .a_a, .b_a и т.д.

Используем область видимости для сокращения объема данных

CSS-минификатор CSSO включает в себя настройку scopes. Созданная область видимости объединяет классы в зависимости от компонента, которому принадлежит класс.

Примените эту настройку, использовав csso-webpack-plugin:

const getScopes = (ast) => {	
  const scopes = {};

  const getModuleID = (className) => {
    const tokens = className.split('_')[0];
  
    if (tokens.length !== 2) {
      return 'default';
    }

    return tokens[0];
  };

  csso.syntax.walk(ast, node => {
    if (node.type === 'ClassSelector') {
      const moduleId = getModuleID(node.name);

      if (moduleId) {
        if (!scopes[moduleId]) {
          scopes[moduleId] = [];
        }

        if (!scopes[moduleId].includes(node.name)) {
          scopes[moduleId].push(node.name);
        }
      }
    }
  });

  return Object.values(scopes);
};

Это сократило объем CSS-кода в GO2CINEMA с 53KB до 47 KB.

Стоит ли оно того?

Первый аргумент против такой минификации заключается в том, что алгоритмы компрессии все сделают за тебя. Если сжать CSS-код GO2CINEMA, используя алгоритм Brotli, то это уменьшит размер данных всего на 1 Кб.

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

Данная публикация представляет собой перевод статьи «Reducing CSS bundle size 70% by cutting the class names and using scope isolation» , подготовленной дружной командой проекта Интернет-технологии.ру