HMR для CSS и CSS-модулей

Hot Module Replacement (HMR) в Vite основан на нативной поддержке ES-модулей и WebSocket-соединении между dev-сервером и браузером. В контексте CSS это означает возможность обновления стилей без полной перезагрузки страницы и без потери состояния приложения.

CSS рассматривается Vite как особый тип модуля, который не требует JavaScript-обёртки для применения изменений. При изменении файла стилей Vite генерирует обновлённый модуль и отправляет сигнал клиенту, после чего браузер заменяет соответствующий <style> или <link> ресурс.

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

Как Vite обрабатывает CSS в dev-режиме

При импорте CSS в Jav * aScript:

import './styles.css'

Vite выполняет несколько шагов:

  1. Парсинг импорта как зависимости модуля
  2. Преобразование CSS в внутренний модуль Vite
  3. Инъекция стилей через <style> в документ
  4. Регистрация зависимости для HMR-графа

Каждый CSS-файл становится наблюдаемым узлом в модульном графе Vite. Это позволяет точно определять, какие части приложения должны обновиться при изменении файла.

Обновление CSS без перезагрузки страницы

При изменении CSS-файла Vite выполняет следующие действия:

  • генерирует новый CSS-бандл для изменённого модуля
  • отправляет HMR-событие через WebSocket
  • клиентский runtime определяет тип обновления как style-update
  • старые стили удаляются или заменяются новыми

В отличие от JavaScript HMR, CSS не требует выполнения accept-логики. Обновление происходит автоматически.

Поведение при изменении импортированных цепочек

CSS часто импортируется через цепочку модулей:

import './base.css'
import './layout.css'
import './theme.css'

При изменении одного файла Vite:

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

Это обеспечивает минимальный радиус обновления и стабильное состояние интерфейса.

HMR и каскадность CSS

CSS по своей природе зависит от каскада и специфичности. HMR в Vite учитывает это косвенно:

  • обновление стилей происходит целиком для файла
  • порядок подключения сохраняется согласно графу импортов
  • повторная вставка <style> происходит с учётом исходной позиции

Если порядок импорта меняется, Vite может пересоздать часть style-инъекций, чтобы сохранить корректный cascade.

CSS Modules и их HMR-поведение

CSS Modules в Vite используют суффикс .module.css:

import styles from './button.module.css'

В этом случае CSS обрабатывается иначе:

  • классы хэшируются для локальной области видимости
  • экспортируется JS-объект с маппингом классов
  • HMR обновляет не только стили, но и JS-объект

Обновление CSS Modules

При изменении файла .module.css происходит двойное обновление:

  1. CSS-часть обновляется как обычный стиль
  2. JS-экспорт пересоздаётся с новыми хэшами классов

Vite отправляет HMR-событие, которое приводит к:

  • обновлению стилей в DOM
  • обновлению импортированного объекта styles
  • повторному рендеру компонентов, использующих изменённые классы

Пример:

import styles from './button.module.css'

button.className = styles.primary

После изменения CSS Module объект styles.primary может получить новый хэш, и Vite гарантирует, что импорт обновится без перезагрузки страницы.

Стабильность ссылок в CSS Modules

HMR в CSS Modules имеет важное ограничение: имена классов могут изменяться при каждом обновлении, если изменяется содержимое файла.

Это приводит к следующим особенностям:

  • нельзя кэшировать значения классов вне реактивного контекста
  • ссылки на styles.* должны оставаться актуальными
  • статическое сохранение className вне компонента может привести к рассинхронизации

Взаимодействие CSS Modules с JavaScript HMR

CSS Modules тесно связаны с JS HMR, поскольку экспортируются как объект:

export default {
  button: "button_hash_123"
}

При изменении CSS:

  • модуль пересобирается
  • экспорт обновляется
  • зависимые JS-модули получают HMR update
  • вызывается перерендер компонентов (в рамках фреймворка, если он поддерживает HMR)

Таким образом CSS Modules фактически являются гибридным типом модуля.

Инвалидация и зависимые модули

Vite строит граф зависимостей, где CSS и CSS Modules могут иметь следующие связи:

  • JS → CSS
  • JS → CSS Module → CSS
  • CSS → импорт других CSS (через @import)

При изменении одного узла:

  • инвалидируются только зависимые ветви
  • минимизируется количество обновлений DOM
  • предотвращается полная перезагрузка приложения

Особенно важно поведение при @import:

@import './reset.css';

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

Особенности работы с @import

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

  • каждый импортируемый файл становится отдельным модулем Vite
  • HMR работает на уровне каждого файла
  • обновление одного импортируемого CSS не требует пересборки всего файла-родителя

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

Инъекция стилей в DOM

Vite управляет стилями через динамическую вставку:

  • создаётся <style> тег для каждого CSS-модуля
  • теги группируются по идентификаторам модулей
  • при обновлении старый тег заменяется новым

Для CSS Modules структура более сложная:

  • отдельный стиль для модуля
  • отдельный JS-объект для экспортов
  • синхронное обновление обоих слоёв

Ограничения CSS HMR

Несмотря на высокую скорость обновления, существуют ограничения:

  • не сохраняется состояние CSS-анимаций при замене стилей
  • возможны кратковременные визуальные «мигания»
  • сложные зависимости через @import могут приводить к перестройке нескольких модулей
  • изменение глобальных стилей влияет на весь документ без дифференциации компонентов

CSS Modules уменьшают часть этих проблем за счёт локализации классов, но не устраняют их полностью.

Производительность обновлений

Vite оптимизирует CSS HMR следующим образом:

  • обновляются только изменённые модули
  • используется кэширование преобразований
  • минимизируется пересоздание DOM-узлов <style>
  • WebSocket-события имеют узкую спецификацию (style-update, css-module-update)

Это обеспечивает обновление стилей практически без задержек даже в больших проектах.

Влияние фреймворков на CSS HMR

Поведение CSS HMR может отличаться в зависимости от используемого фреймворка:

  • React: обновление вызывает повторный рендер компонентов, использующих CSS Modules
  • Vue: реактивная система автоматически подхватывает новые классы
  • Svelte: компиляция компонентов позволяет более прямое обновление стилей

Однако сам механизм Vite остаётся одинаковым — обновление модулей через HMR runtime.

Граничные случаи

Некоторые ситуации требуют особого поведения:

  • динамическая генерация className через JS
  • использование CSS variables, изменяемых в runtime
  • сочетание глобальных и модульных стилей
  • условные импорты CSS

В таких случаях HMR работает корректно на уровне файлов, но логика применения классов зависит от архитектуры приложения.

Итоговое поведение системы HMR для CSS и CSS Modules

Vite рассматривает CSS как полноценный модуль первого класса, а CSS Modules как расширенный тип модуля с дополнительным JS-слоем. Это позволяет объединить:

  • мгновенное обновление стилей
  • точечную инвалидацию зависимостей
  • синхронизацию CSS и JS состояния
  • минимальное вмешательство в DOM

Такой подход делает CSS HMR не отдельной подсистемой, а частью общего графа модулей приложения.