Постоянное кэширование (Persistent Caching)

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

Внутренняя система кэширования Webpack работает на нескольких уровнях:

  • кэширование модулей и их трансформаций
  • кэширование результатов резолва (resolve)
  • кэширование loader-ов
  • файловый кэш сборки (persistent cache)
  • кэширование snapshot состояния файлов

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

Основной принцип: если входные данные модуля и окружение не изменились, результат его обработки может быть переиспользован.

Persistent filesystem cache

Ключевая функциональность Webpack 5 — встроенный файловый кэш, который сохраняет данные сборки между процессами.

Конфигурация включает базовый блок:

module.exports = {
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
    buildDependencies: {
      config: [__filename]
    }
  }
}

Принцип работы

При первой сборке Webpack:

  1. Строит dependency graph
  2. Выполняет loader chain для каждого модуля
  3. Выполняет трансформации (Babel, TS, CSS loaders и т.д.)
  4. Сохраняет результаты в кэш

При повторной сборке:

  1. Проверяет валидность кэша
  2. Сравнивает хэши входных файлов
  3. Сравнивает зависимости конфигурации
  4. Загружает готовые результаты вместо пересчёта

Инвалидация кэша

Кэш становится невалидным при изменении следующих факторов:

  • исходный файл модуля
  • зависимости модуля
  • конфигурация Webpack
  • версии loader-ов и plugins
  • переменные окружения, влияющие на сборку
  • параметры resolve

Особое внимание уделяется buildDependencies:

buildDependencies: {
  config: [__filename]
}

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

Content hashing и связь с кэшированием

Хотя persistent cache работает на уровне сборщика, contenthash влияет на итоговые артефакты.

Разделение:

  • cache — ускоряет процесс сборки
  • contenthash — обеспечивает кэширование браузером

Пример:

output: {
  filename: '[name].[contenthash].js'
}

Если содержимое модуля не изменилось, Webpack может использовать кэшированную трансформацию и в итоге сохранить тот же contenthash.

Кэширование модулей и loader pipeline

Каждый модуль проходит цепочку loader-ов. Webpack сохраняет промежуточные результаты:

  • исходный код после loader A
  • AST после loader B
  • финальный JS после loader C

Если входной файл и конфигурация loader-ов не изменились, pipeline не выполняется заново.

Пример:

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

В Webpack 5 cache-loader часто избыточен, поскольку filesystem cache уже покрывает эту задачу.

Resolver cache

Отдельный слой кэширования относится к resolve алгоритму.

Webpack кеширует:

  • пути к модулям (import 'react')
  • алиасы (@components/Button)
  • расширения (.js, .ts, .json)

Это позволяет избежать повторного обхода файловой системы.

Пример настройки resolve:

resolve: {
  extensions: ['.js', '.ts'],
  alias: {
    '@': path.resolve(__dirname, 'src')
  }
}

Каждый resolve запрос кэшируется с учётом контекста.

Snapshot система

Webpack использует snapshot механизм для отслеживания состояния файловой системы.

Snapshot включает:

  • timestamp файла
  • размер файла
  • hash содержимого (в некоторых режимах)
  • список зависимостей

Если snapshot совпадает с предыдущим состоянием, файл считается неизменным и не пересобирается.

Это особенно важно при инкрементальной сборке.

Оптимизация cache buildDependencies

Неправильная настройка buildDependencies может полностью обесценить persistent cache.

Типичная ошибка:

buildDependencies: {
  config: []
}

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

Оптимальный вариант:

buildDependencies: {
  config: [__filename],
  tsconfig: [path.resolve(__dirname, 'tsconfig.json')]
}

Cache invalidation в монорепозиториях

В монорепозиториях (Lerna, Nx, pnpm workspaces) проблема кэширования усложняется:

  • общий Webpack config
  • множество пакетов
  • пересекающиеся зависимости

Ключевой риск — чрезмерная инвалидизация кэша.

Решение:

  • разделение cacheDirectory по пакетам
  • использование уникальных cache keys
  • явное указание buildDependencies для каждого workspace

Пример:

cache: {
  type: 'filesystem',
  cacheDirectory: path.resolve(__dirname, '.cache', packageName),
  name: packageName
}

Влияние loader-ов на кэш

Не все loader-ы одинаково хорошо кэшируются.

Проблемные случаи:

  • loader с случайными значениями (Math.random, Date.now)
  • loader, зависящий от внешнего API
  • loader без детерминированного результата

Такие loader-ы полностью ломают кэширование.

Правильный подход — детерминированные трансформации.

Минимизация cache misses

Основные причины cache miss:

  • изменение порядка loader-ов
  • изменение версии зависимостей
  • динамическая конфигурация Webpack
  • нестабильные пути (absolute vs relative)
  • отсутствие нормализации путей

Оптимизация включает:

  • фиксированные версии пакетов
  • стабильную структуру конфигурации
  • минимизацию динамических условий в webpack.config.js

Производительность filesystem cache

Filesystem cache ускоряет сборку за счёт:

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

Однако при очень больших проектах возникают узкие места:

  • I/O операции диска
  • размер cache directory
  • стоимость сериализации AST

В таких случаях используется разделение кэша по средам (dev/build/ci).

Cache в режиме development

В development режиме cache особенно важен при HMR.

Webpack сохраняет:

  • состояние модулей
  • dependency graph
  • runtime модули

Это позволяет обновлять только изменённые части приложения без полной пересборки.

Cache и performance budget

Persistent cache напрямую влияет на performance budget проекта:

  • сокращает cold build time
  • уменьшает time-to-first-build
  • снижает нагрузку на CPU при повторных запусках

Но при этом увеличивает:

  • использование диска
  • сложность отладки сборки

Практические проблемы

На практике persistent cache может приводить к скрытым проблемам:

  • «залипшие» артефакты при неправильной invalidation логике
  • несоответствие dev/prod сборок
  • трудности воспроизведения багов

В таких случаях часто используется временное отключение cache:

cache: false

или очистка директории .webpack-cache.

Cache и CI/CD

В CI системах кэширование требует аккуратной настройки:

  • кэш должен быть изолирован от разных веток
  • необходимо учитывать hash коммитов
  • важно избегать пересечения окружений

Часто применяется стратегия:

  • cache key = hash(package.json + lockfile + webpack config)
  • восстановление cache перед сборкой
  • сохранение cache после успешной сборки

Это значительно ускоряет pipeline при повторных прогонах.