MiniCssExtractPlugin: вынос CSS в отдельные файлы

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

Без использования плагина CSS может внедряться в страницу через JavaScript с помощью style-loader. Такой подход удобен во время разработки, однако для production он имеет ряд недостатков:

  • CSS загружается только после выполнения JavaScript;
  • отсутствуют полноценное кеширование CSS-файлов;
  • увеличивается размер JS-бандлов;
  • ухудшается скорость первого рендера;
  • браузер не может параллельно загружать стили и скрипты.

MiniCssExtractPlugin решает эти проблемы, создавая отдельные .css-файлы.


Установка

npm install mini-css-extract-plugin --save-dev

Базовая настройка

Минимальная конфигурация включает:

  • подключение плагина;
  • замену style-loader на MiniCssExtractPlugin.loader;
  • настройку имени CSS-файлов.
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',

  entry: './src/index.js',

  output: {
    filename: 'js/main.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/styles.css',
    }),
  ],
};

Как работает извлечение CSS

Когда JavaScript импортирует CSS:

import './styles/main.css';

Webpack:

  1. обрабатывает CSS через css-loader;
  2. преобразует CSS в модуль;
  3. передаёт содержимое в MiniCssExtractPlugin.loader;
  4. плагин собирает весь CSS;
  5. создаёт отдельный CSS-файл.

В результате:

dist/
  js/
    main.js
  css/
    styles.css

Отличие от style-loader

style-loader

use: [
  'style-loader',
  'css-loader',
]

Поведение:

  • CSS внедряется в <style>;
  • стили появляются через JavaScript;
  • удобен для HMR;
  • подходит для development.

MiniCssExtractPlugin

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
]

Поведение:

  • CSS выносится в файл;
  • браузер загружает CSS отдельно;
  • лучше кеширование;
  • быстрее загрузка страницы;
  • используется в production.

Комбинация development и production режимов

На практике часто используют:

  • style-loader в development;
  • MiniCssExtractPlugin в production.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          isDev
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },

  plugins: [
    ...(!isDev
      ? [
          new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash].css',
          }),
        ]
      : []),
  ],
};

Генерация CSS для каждого entry

Если приложение имеет несколько entry points:

entry: {
  home: './src/home.js',
  admin: './src/admin.js',
},

можно генерировать отдельный CSS для каждого бандла:

new MiniCssExtractPlugin({
  filename: 'css/[name].css',
})

Результат:

dist/
  css/
    home.css
    admin.css

Использование contenthash

Production-сборки обычно используют хеширование файлов:

new MiniCssExtractPlugin({
  filename: 'css/[name].[contenthash].css',
})

Пример:

main.a7f3c1.css

contenthash меняется только при изменении содержимого CSS.

Преимущества:

  • долгосрочное кеширование;
  • браузер не скачивает CSS повторно без изменений;
  • уменьшается сетевой трафик.

Извлечение SCSS

Плагин не ограничивается обычным CSS.

Для SCSS:

npm install sass sass-loader --save-dev

Конфигурация:

{
  test: /\.scss$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'sass-loader',
  ],
}

Импорт:

import './styles/app.scss';

Извлечение Less

npm install less less-loader --save-dev
{
  test: /\.less$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'less-loader',
  ],
}

Извлечение Stylus

npm install stylus stylus-loader --save-dev
{
  test: /\.styl$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'stylus-loader',
  ],
}

Работа с CSS Modules

Конфигурация

{
  test: /\.module\.css$/,
  use: [
    MiniCssExtractPlugin.loader,
    {
      loader: 'css-loader',
      options: {
        modules: true,
      },
    },
  ],
}

Использование

.title {
  color: red;
}
import styles from './App.module.css';

element.className = styles.title;

Настройка localIdentName

Для production:

modules: {
  localIdentName: '[hash:base64]',
}

Для development:

modules: {
  localIdentName: '[path][name]__[local]',
}

Разделение CSS по чанкам

При использовании динамического импорта:

import('./profile');

Webpack создаёт отдельный JS chunk.

MiniCssExtractPlugin также создаёт CSS для этого чанка:

profile.js
profile.css

Это особенно важно для:

  • lazy loading;
  • route splitting;
  • больших SPA-приложений.

chunkFilename

Для CSS-чанков используется отдельная настройка:

new MiniCssExtractPlugin({
  filename: 'css/[name].css',
  chunkFilename: 'css/[id].css',
})

или:

chunkFilename: 'css/[name].[contenthash].css',

Игнорирование порядка CSS

Иногда появляется предупреждение:

Conflicting order between:

Причина — Webpack не может гарантировать одинаковый порядок подключения CSS-модулей.

Для отключения предупреждения:

new MiniCssExtractPlugin({
  ignoreOrder: true,
})

Однако это не устраняет проблему порядка, а только скрывает предупреждение.


Причины конфликтов порядка

Проблема обычно возникает при:

  • разных путях импорта CSS;
  • повторном импорте модулей;
  • динамических чанках;
  • смешивании global CSS и CSS Modules.

Пример:

import './a.css';
import './b.css';

в одном файле и:

import './b.css';
import './a.css';

в другом.

Webpack не знает, какой порядок считать правильным.


Правильная организация CSS

Для минимизации конфликтов обычно:

  • создают единый entry CSS;
  • используют глобальный reset первым;
  • компоненты импортируют локально;
  • избегают случайных повторных импортов.

publicPath

Иногда CSS содержит ссылки на изображения:

background-image: url('../images/bg.png');

Если ресурсы загружаются некорректно, помогает настройка publicPath.

{
  loader: MiniCssExtractPlugin.loader,
  options: {
    publicPath: '../',
  },
}

Пример структуры проекта

src/
  styles/
    main.scss
  images/
    logo.png
dist/
  css/
    main.css
  images/
    logo.png

Если CSS лежит глубже, относительные пути должны корректироваться через publicPath.


ES-модули

По умолчанию loader использует ES Modules.

При необходимости можно отключить:

{
  loader: MiniCssExtractPlugin.loader,
  options: {
    esModule: false,
  },
}

Обычно это требуется при совместимости со старыми конфигурациями.


Hot Module Replacement

Исторически HMR для CSS через MiniCssExtractPlugin работал нестабильно.

Современный Webpack поддерживает HMR лучше, однако наиболее распространённая практика остаётся такой:

  • development → style-loader;
  • production → MiniCssExtractPlugin.

Совместимость с source maps

Для генерации source maps:

module.exports = {
  devtool: 'source-map',
};

и:

{
  loader: 'css-loader',
  options: {
    sourceMap: true,
  },
},
{
  loader: 'sass-loader',
  options: {
    sourceMap: true,
  },
},

Минификация CSS

MiniCssExtractPlugin не минифицирует CSS.

Для минификации используется css-minimizer-webpack-plugin.

Установка:

npm install css-minimizer-webpack-plugin --save-dev

Конфигурация:

const CssMinimizerPlugin = require(
  'css-minimizer-webpack-plugin'
);

module.exports = {
  optimization: {
    minimizer: [
      '...',
      new CssMinimizerPlugin(),
    ],
  },
};

Полноценная production-конфигурация

const path = require('path');
const MiniCssExtractPlugin = require(
  'mini-css-extract-plugin'
);

const CssMinimizerPlugin = require(
  'css-minimizer-webpack-plugin'
);

module.exports = {
  mode: 'production',

  entry: './src/index.js',

  output: {
    filename: 'js/[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },

  module: {
    rules: [
      {
        test: /\.(scss|css)$/,

        use: [
          MiniCssExtractPlugin.loader,

          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },

          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
    ],
  },

  optimization: {
    minimizer: [
      '...',
      new CssMinimizerPlugin(),
    ],
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename:
        'css/[name].[contenthash].css',
    }),
  ],

  devtool: 'source-map',
};

Что происходит после сборки

После выполнения:

npx webpack

Webpack:

  1. собирает все CSS-импорты;
  2. извлекает стили из JS;
  3. объединяет CSS;
  4. создаёт отдельные файлы;
  5. генерирует source maps;
  6. минимизирует CSS;
  7. связывает CSS с HTML.

Интеграция с HtmlWebpackPlugin

При использовании:

new HtmlWebpackPlugin()

Webpack автоматически подключает CSS:

<link href="css/main.css" rel="stylesheet">

Ручное подключение обычно не требуется.


Типичные ошибки

Использование style-loader вместе с MiniCssExtractPlugin

Неверно:

use: [
  'style-loader',
  MiniCssExtractPlugin.loader,
  'css-loader',
]

Нужно использовать только один loader.


Неправильный порядок loaders

Неверно:

use: [
  'css-loader',
  MiniCssExtractPlugin.loader,
]

Правильно:

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
]

Webpack выполняет loaders справа налево.


Отсутствие css-loader

MiniCssExtractPlugin не умеет самостоятельно обрабатывать CSS.

Нужен css-loader.


Отсутствие plugin в plugins

Loader без плагина работать не будет.

Неверно:

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
]

без:

plugins: [
  new MiniCssExtractPlugin(),
]

Производительность и преимущества

Использование MiniCssExtractPlugin даёт:

  • уменьшение JS-бандлов;
  • более быстрый рендер;
  • параллельную загрузку ресурсов;
  • эффективное кеширование;
  • оптимизацию production-сборок;
  • поддержку code splitting;
  • независимое обновление CSS-файлов.

Для production Webpack-проектов плагин фактически является стандартом де-факто при работе со стилями.