cache-loader (устаревший) и переход на встроенный кэш

Сборка фронтенд-приложений представляет собой цепочку преобразований модулей: загрузка исходников, транспиляция JavaScript/TypeScript, обработка стилей, оптимизация и бандлинг. При увеличении проекта стоимость этих операций становится критичной, особенно в режиме разработки, где пересборка запускается многократно.

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

  • уровень загрузчиков (loader-level cache)
  • уровень компилятора (compiler-level cache)
  • внешний кеш (инструменты и плагины)

До появления встроенного механизма кеширования в Webpack 5 основным способом ускорения loader-цепочек был cache-loader.


cache-loader: назначение и принцип работы

cache-loader использовался для сохранения результатов работы последующих loader-ов в файловый кеш. Он вставлялся в цепочку загрузчиков и кешировал результат обработки модуля на основе входного содержимого и конфигурации.

Базовая идея

При первом проходе модуль проходит через цепочку loaders:

source code → loader A → loader B → loader C → output

cache-loader сохраняет результат работы последующих loaders:

source code → cache-loader → loader A → loader B → loader C → cache write

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

source code → cache-loader → cache hit → cached output

Пример конфигурации cache-loader

Типичный пример использования в Webpack 4:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'cache-loader',
            options: {
              cacheDirectory: path.resolve(__dirname, '.cache-loader')
            }
          },
          {
            loader: 'babel-loader'
          }
        ]
      }
    ]
  }
};

В этом сценарии cache-loader сохранял результат работы babel-loader, существенно ускоряя повторные сборки.


Механизм работы кеша

Ключ кеша формировался на основе:

  • содержимого исходного файла
  • списка loader-ов и их конфигурации
  • зависимостей модуля (частично)
  • окружения сборки (частично)

Результат сериализовался и сохранялся на диск. При повторной сборке происходило сравнение ключей, и при совпадении возвращался готовый результат без повторного выполнения loader-цепочки.


Ограничения cache-loader

Со временем подход с cache-loader начал демонстрировать архитектурные ограничения.

1. Локальный кеш только для loader-цепочек

cache-loader кешировал только часть пайплайна — результаты loader-ов. Он не учитывал весь граф зависимостей Webpack, что приводило к:

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

2. Дублирование функциональности

С появлением более продвинутых инструментов кеширования внутри Webpack сам подход стал избыточным:

  • Webpack начал поддерживать persistent cache
  • loader-ы получили собственные кеш-механизмы (babel-loader, ts-loader)
  • возникло пересечение ответственности между уровнями системы

3. Дополнительные накладные расходы

Сам cache-loader добавлял:

  • дополнительный слой I/O операций
  • сериализацию/десериализацию данных
  • увеличение сложности loader-chain

В некоторых сценариях он даже ухудшал производительность при небольших проектах.


4. Отсутствие актуального развития

Пакет перестал развиваться в пользу встроенных механизмов Webpack 5, что закрепило его статус устаревшего решения.


Встроенный кеш Webpack 5

Webpack 5 представил нативную систему кеширования, интегрированную в компилятор. Она работает на уровне всей сборки, а не отдельных loader-ов.

Основные режимы:

  • memory — кеш в оперативной памяти
  • filesystem — персистентный кеш на диске

Настройка filesystem cache

Наиболее практичный вариант для реальных проектов:

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

Ключевые параметры

type: ‘filesystem’

Активирует сохранение кеша на диск. Позволяет ускорять повторные сборки даже после перезапуска процесса.

cacheDirectory

Определяет директорию хранения кеша. Может быть общей для CI или изолированной для каждого проекта.

buildDependencies

Определяет зависимости конфигурации, при изменении которых кеш инвалидируется. Обычно включает:

  • webpack config
  • env-файлы
  • плагины

Как работает встроенный кеш

Webpack 5 кеширует значительно более широкий контекст, чем cache-loader:

  • модули и их трансформации
  • результаты loader-цепочек
  • dependency graph
  • результаты оптимизации
  • chunk graph

Кеш строится на основе хеширования графа сборки, а не отдельных файлов.


Переход с cache-loader на встроенный кеш

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

Удаление cache-loader из цепочек

Было:

use: [
  'cache-loader',
  'babel-loader'
]

Стало:

use: [
  'babel-loader'
]

Включение cache в webpack config

Добавляется глобальный кеш:

cache: {
  type: 'filesystem'
}

Проверка loader-level кешей

Некоторые loader-ы уже имеют собственные кеши, и их комбинация с filesystem cache может быть избыточной:

  • babel-loadercacheDirectory
  • ts-loadertranspileOnly + incremental build
  • eslint-loader (устаревший) → cache в eslint

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


Взаимодействие с Babel и TypeScript

Babel

babel-loader имеет собственный механизм кеширования:

{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true
  }
}

При использовании Webpack filesystem cache этот параметр становится опциональным. В больших проектах возможны два подхода:

  • оставить cacheDirectory для ускорения трансформации AST
  • отключить для упрощения диагностики

TypeScript

ts-loader поддерживает incremental compilation:

{
  loader: 'ts-loader',
  options: {
    transpileOnly: true
  }
}

В связке с Webpack cache это снижает нагрузку на типизацию, оставляя Webpack управление кешем модулей.


Инвалидация кеша и управление зависимостями

Одной из ключевых задач встроенного кеша является корректная инвалидация.

Webpack отслеживает:

  • изменения исходных файлов
  • изменения конфигурации
  • изменения loader-ов
  • изменения зависимостей через require/import

Однако внешние зависимости требуют явного указания:

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

Без этого возможны ситуации, когда кеш сохраняет устаревшие результаты.


CI и кеширование

Filesystem cache особенно эффективен в CI/CD:

  • кеш можно сохранять между билдами
  • ускоряется сборка pull request-ов
  • уменьшается нагрузка на pipeline

Типичная практика:

  • отдельная директория кеша на job
  • восстановление кеша перед сборкой
  • сохранение после завершения

Типичные ошибки при переходе

1. Оставленный cache-loader в цепочке

Приводит к двойному кешированию и неожиданным задержкам.


2. Отсутствие buildDependencies

Кеш не инвалидируется при изменении конфигурации.


3. Избыточные loader-кеши

Комбинация:

  • cache-loader
  • babel-loader cacheDirectory
  • webpack filesystem cache

создаёт ненужный overhead.


4. Неправильный cacheDirectory

Общий кеш для разных проектов может приводить к конфликтам хешей и некорректным результатам сборки.


Эволюция подхода к кешированию

Переход от cache-loader к встроенному кешу отражает общую архитектурную тенденцию Webpack:

  • от распределённого кеширования в loader-ах
  • к централизованному кешированию компилятора
  • с учётом всего dependency graph

Это уменьшает фрагментацию логики и повышает предсказуемость сборки.


Практическая модель современного кеширования

В актуальных конфигурациях Webpack 5 базовая схема выглядит так:

  • Webpack filesystem cache управляет результатами сборки
  • loader-ы выполняют минимально необходимую работу
  • локальные кеши используются точечно, только при доказанной выгоде
  • вся система опирается на граф зависимостей, а не отдельные файлы