Файловая система кэширования в Webpack 5: filesystem cache

До появления Webpack 5 основным механизмом ускорения повторных сборок был cache-loader, а также внутренний in-memory cache. Эти подходы работали ограниченно:

  • кэш исчезал после завершения процесса;
  • повторный запуск Webpack требовал заново пересобирать проект;
  • большие проекты с Babel, TypeScript и PostCSS оставались медленными;
  • инкрементальные сборки занимали значительное время.

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

Главная цель механизма — резко сократить время повторных сборок.


Базовое включение filesystem cache

Кэширование включается через секцию cache.

module.exports = {
    cache: {
        type: 'filesystem'
    }
};

После активации Webpack начинает сохранять кэшированные данные в специальную директорию.

По умолчанию каталог располагается внутри:

node_modules/.cache/webpack

Что именно кэширует Webpack

Filesystem cache сохраняет:

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

Повторная сборка использует уже готовые результаты вместо повторного вычисления.


Разница между memory cache и filesystem cache

Memory cache

cache: {
    type: 'memory'
}

Особенности:

  • хранится только в RAM;
  • работает быстро;
  • очищается после завершения процесса;
  • полезен для dev-server.

Filesystem cache

cache: {
    type: 'filesystem'
}

Особенности:

  • сохраняется между перезапусками;
  • ускоряет cold start;
  • особенно эффективен для больших проектов;
  • требует дискового пространства.

Как работает механизм инвалидирования кэша

Webpack должен понимать, когда кэш становится невалидным.

Для этого анализируются:

  • конфигурация Webpack;
  • версии loaders;
  • зависимости;
  • содержимое файлов;
  • environment variables;
  • build dependencies.

Если изменяется что-либо значимое — кэш пересоздаётся.


Настройка cache.buildDependencies

Webpack отслеживает файлы, влияющие на сборку.

Обычно сюда добавляют webpack-конфиг:

module.exports = {
    cache: {
        type: 'filesystem',
        buildDependencies: {
            config: [__filename]
        }
    }
};

Теперь изменение webpack.config.js автоматически инвалидирует кэш.


Почему buildDependencies важен

Без корректного списка зависимостей возможно:

  • использование устаревшего кэша;
  • некорректная сборка;
  • игнорирование изменений конфигурации;
  • проблемы при обновлении aliases или loaders.

Особенно критично это для:

  • monorepo;
  • динамических конфигов;
  • кастомных loaders;
  • shared webpack presets.

Кэширование в больших проектах

На крупных проектах filesystem cache даёт максимальный эффект.

Типичные источники замедления:

  • Babel;
  • TypeScript;
  • SCSS;
  • PostCSS;
  • source maps;
  • tree shaking;
  • минификация.

Без кэша повторные сборки могут занимать десятки секунд.

С filesystem cache:

  • cold build остаётся тяжёлым;
  • warm build ускоряется в разы.

Настройка cacheDirectory

Каталог хранения можно изменить.

const path = require('path');

module.exports = {
    cache: {
        type: 'filesystem',
        cacheDirectory: path.resolve(__dirname, '.temp_cache')
    }
};

Это полезно:

  • при CI/CD;
  • в Docker;
  • в monorepo;
  • при разделении кэша между окружениями.

Структура cacheDirectory

Внутри директории создаются:

.cache/
└── webpack/
    ├── default-development/
    ├── default-production/
    └── index.pack

Webpack хранит:

  • сериализованные объекты;
  • бинарные пакеты;
  • snapshots;
  • metadata;
  • module cache.

Параметр name

Опция name позволяет разделять кэши.

module.exports = {
    cache: {
        type: 'filesystem',
        name: 'client-cache'
    }
};

Полезно при:

  • нескольких конфигурациях;
  • client/server builds;
  • SSR;
  • multi-compiler mode.

Использование cache.name в multi-compiler

module.exports = [
    {
        name: 'client',
        cache: {
            type: 'filesystem',
            name: 'client'
        }
    },

    {
        name: 'server',
        cache: {
            type: 'filesystem',
            name: 'server'
        }
    }
];

Без разделения возможны конфликты кэша.


Опция version

Позволяет вручную инвалидировать кэш.

module.exports = {
    cache: {
        type: 'filesystem',
        version: '1.0.0'
    }
};

После изменения версии Webpack создаст новый кэш.


Когда использовать version

Практические сценарии:

  • изменение внутренней логики loaders;
  • обновление build pipeline;
  • нестабильные plugins;
  • смена окружения;
  • миграция инфраструктуры.

Пример:

version: process.env.BUILD_VERSION

Опция compression

Webpack умеет сжимать кэш.

module.exports = {
    cache: {
        type: 'filesystem',
        compression: 'gzip'
    }
};

Варианты:

  • false
  • 'gzip'

Плюсы и минусы compression

Без сжатия

Плюсы:

  • быстрее запись;
  • быстрее чтение.

Минусы:

  • больше размер кэша.

gzip

Плюсы:

  • меньше файлов;
  • экономия места.

Минусы:

  • дополнительная CPU-нагрузка;
  • более медленная сериализация.

Параметр cacheLocation

Позволяет полностью переопределить путь.

const path = require('path');

module.exports = {
    cache: {
        type: 'filesystem',
        cacheLocation: path.resolve(
            __dirname,
            '.webpack-cache'
        )
    }
};

Разница:

  • cacheDirectory — базовая директория;
  • cacheLocation — полный путь.

Опция store

Определяет формат хранения.

module.exports = {
    cache: {
        type: 'filesystem',
        store: 'pack'
    }
};

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

pack

Webpack объединяет множество данных в pack-файлы.


Snapshot system

Filesystem cache тесно связан с системой snapshots.

Snapshot — снимок состояния файловой системы.

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

  • timestamps;
  • hashes;
  • наличие файлов;
  • symlink;
  • package metadata.

managedPaths

Некоторые директории считаются управляемыми package manager.

module.exports = {
    snapshot: {
        managedPaths: [
            /node_modules/
        ]
    }
};

Webpack предполагает, что содержимое редко меняется.

Это ускоряет проверки.


immutablePaths

Полностью неизменяемые директории.

module.exports = {
    snapshot: {
        immutablePaths: [
            /cache/
        ]
    }
};

Webpack практически перестаёт проверять такие пути.


Влияние filesystem cache на HMR

Hot Module Replacement и filesystem cache работают совместно.

Кэш:

  • ускоряет initial compilation;
  • ускоряет rebuild;
  • уменьшает повторную обработку loaders.

Но HMR всё равно пересобирает изменённые модули.


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

Webpack 5 способен заменить cache-loader.

Пример:

module.exports = {
    cache: {
        type: 'filesystem'
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    }
};

Дополнительный cache-loader обычно больше не нужен.


Babel loader cacheDirectory

У babel-loader существует собственный кэш:

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

Теперь возникает два уровня кэширования:

  • Babel cache;
  • Webpack filesystem cache.

Нужно ли использовать Babel cache вместе с filesystem cache

Обычно:

Webpack filesystem cache достаточно

Но возможны сценарии комбинирования:

  • legacy build systems;
  • сложные Babel plugins;
  • нестандартные пайплайны;
  • отдельный запуск Babel вне Webpack.

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

Filesystem cache особенно полезен вместе с:

  • ts-loader
  • babel-loader + preset-typescript
  • fork-ts-checker-webpack-plugin

Пример:

module.exports = {
    cache: {
        type: 'filesystem'
    },

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

Кэш и source maps

Генерация source maps — одна из самых дорогих операций.

Filesystem cache помогает избежать повторной генерации.

Особенно эффективно с:

devtool: 'source-map'

или

devtool: 'cheap-module-source-map'

Очистка кэша

Иногда требуется ручное удаление.

Типичный путь:

node_modules/.cache/webpack

Linux/macOS:

rm -rf node_modules/.cache/webpack

Windows PowerShell:

Remove-Item node_modules/.cache/webpack -Recurse -Force

Причины повреждения кэша

Возможные проблемы:

  • обновление Node.js;
  • изменение loader internals;
  • несовместимые plugins;
  • ошибки сериализации;
  • аварийное завершение сборки;
  • проблемы Docker volumes.

Признаки проблемного кэша

Типичные симптомы:

  • сборка ведёт себя нестабильно;
  • изменения не применяются;
  • устаревший output;
  • random build errors;
  • различия между CI и local build.

Часто помогает полная очистка cache directory.


idleTimeout

Webpack умеет откладывать запись кэша.

module.exports = {
    cache: {
        type: 'filesystem',
        idleTimeout: 60000
    }
};

Значение указывается в миллисекундах.


idleTimeoutForInitialStore

Отдельный timeout для первой записи.

module.exports = {
    cache: {
        type: 'filesystem',
        idleTimeoutForInitialStore: 0
    }
};

Полезно для CI и production builds.


allowCollectingMemory

Позволяет освобождать память после сериализации.

module.exports = {
    cache: {
        type: 'filesystem',
        allowCollectingMemory: true
    }
};

Актуально для:

  • больших monorepo;
  • слабых CI runners;
  • ограниченных Docker-контейнеров.

Параметр maxMemoryGenerations

Контролирует количество поколений memory cache.

module.exports = {
    cache: {
        type: 'filesystem',
        maxMemoryGenerations: 10
    }
};

Балансирует:

  • использование RAM;
  • скорость rebuild.

Filesystem cache в development

Наиболее распространённый режим:

module.exports = {
    mode: 'development',

    cache: {
        type: 'filesystem'
    }
};

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

  • быстрый старт dev-server;
  • ускоренный rebuild;
  • меньше CPU-нагрузка;
  • ускорение HMR.

Filesystem cache в production

Использование в production также оправдано.

Особенно:

  • в CI;
  • при incremental builds;
  • в Docker layer cache;
  • в monorepo pipelines.

Кэширование в CI/CD

По умолчанию CI обычно пересоздаёт окружение.

Для реальной выгоды нужно:

  • сохранять .cache;
  • восстанавливать cache artifacts;
  • учитывать lockfile.

Пример путей:

node_modules/.cache/webpack

или кастомный cacheLocation.


Кэш и package-lock.json

Изменение lockfile часто должно инвалидировать кэш.

Например:

const path = require('path');

module.exports = {
    cache: {
        type: 'filesystem',

        buildDependencies: {
            config: [
                __filename,
                path.resolve(__dirname, 'package-lock.json')
            ]
        }
    }
};

Monorepo и filesystem cache

В monorepo возникают дополнительные сложности:

  • shared dependencies;
  • symlink;
  • workspace packages;
  • hoisting;
  • разные webpack configs.

Часто используют:

cache: {
    type: 'filesystem',
    name: 'frontend-app'
}

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

На практике ускорение может достигать:

Тип проекта Ускорение
Небольшой SPA 1.5–2x
React + Babel 3–5x
TypeScript monorepo 5–10x
Большой enterprise build 10x+

Эффект зависит от:

  • числа loaders;
  • размеров dependency graph;
  • SSD;
  • CPU;
  • source maps;
  • tree shaking.

Когда filesystem cache малоэффективен

Механизм почти не помогает:

  • в очень маленьких проектах;
  • при полном изменении dependency graph;
  • при постоянной очистке node_modules;
  • в ephemeral CI;
  • при нестабильных loaders.

Совместимость с plugins

Большинство современных plugins поддерживают filesystem cache корректно.

Но проблемы возможны со старыми plugins:

  • использующими глобальное состояние;
  • работающими с временными файлами;
  • нарушающими deterministic behavior;
  • не поддерживающими serialization.

Логирование cache operations

Для диагностики можно включить инфраструктурное логирование.

module.exports = {
    infrastructureLogging: {
        level: 'verbose'
    }
};

Webpack начнёт выводить:

  • cache hits;
  • invalidation;
  • serialization;
  • restore operations.

Пример полноценной production-конфигурации

const path = require('path');

module.exports = {
    mode: 'production',

    cache: {
        type: 'filesystem',

        name: 'production-cache',

        cacheDirectory: path.resolve(
            __dirname,
            '.webpack-cache'
        ),

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

        compression: 'gzip',

        allowCollectingMemory: true,

        maxMemoryGenerations: 10
    }
};

Практические рекомендации

Для development

cache: {
    type: 'filesystem'
}

Обычно этого достаточно.


Для production

Рекомендуется:

  • отдельный name;
  • сохранение cache artifacts;
  • контроль invalidation;
  • учёт lockfile.

Для monorepo

Желательно:

  • отдельный cache namespace;
  • кастомный cacheDirectory;
  • стабильные paths;
  • корректные snapshots.

Для CI

Важно:

  • сохранять cache между job;
  • использовать lockfile invalidation;
  • избегать постоянной очистки cache directory;
  • учитывать версии Node.js и loaders.