Инвалидация кэша: version, buildDependencies, name

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

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

Webpack 5 предоставляет несколько механизмов контроля инвалидации кэша:

  • cache.version
  • cache.buildDependencies
  • cache.name

Эти параметры особенно важны при использовании файлового кэша (type: 'filesystem').


Как Webpack определяет актуальность кэша

При использовании файлового кэша Webpack сохраняет:

  • результаты трансформации модулей;
  • зависимости между файлами;
  • конфигурацию сборки;
  • версии loader’ов и plugin’ов;
  • информацию о среде сборки.

Перед новой сборкой Webpack проверяет:

  1. Изменились ли входные файлы.
  2. Изменилась ли конфигурация.
  3. Изменились ли зависимости сборки.
  4. Изменились ли параметры самого кэша.

Если хотя бы один фактор отличается — соответствующая часть кэша инвалидируется.

Пример базового файлового кэша:

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

Параметр version

Назначение version

cache.version — это пользовательский идентификатор версии кэша. При изменении значения Webpack полностью сбрасывает существующий кэш и создаёт новый.

Это ручной механизм инвалидации.

Пример:

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

Если изменить значение:

version: '2.0'

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


Когда необходим version

Webpack автоматически отслеживает множество изменений, но не все.

Существуют ситуации, когда логика сборки изменилась, а Webpack этого не понимает:

  • изменился внешний конфигурационный файл;
  • изменилась логика кастомного loader’а;
  • изменилась среда исполнения;
  • изменились переменные окружения;
  • изменился способ генерации контента;
  • изменился набор alias;
  • изменился код, влияющий на трансформацию модулей вне webpack.config.js.

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


Пример с переменными окружения

Допустим, сборка зависит от NODE_ENV.

const env = process.env.NODE_ENV;

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

Теперь development и production будут использовать разные кэши.


Использование динамической версии

Нередко версия строится автоматически:

const path = require('path');
const packageJson = require('./package.json');

module.exports = {
    cache: {
        type: 'filesystem',
        version: `${packageJson.version}-${process.env.NODE_ENV}`
    }
};

Такой подход позволяет:

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

Инвалидация после изменения loader’ов

Предположим, используется собственный loader:

module.exports = function(source) {
    return transform(source);
};

Webpack не всегда способен корректно определить изменение внутренней логики loader’а. Особенно если трансформация зависит от внешних файлов.

В этом случае можно обновлять version вручную:

cache: {
    type: 'filesystem',
    version: 'loader-v3'
}

Стратегии формирования version

Простая строка

version: '1'

Подходит для ручного контроля.


Версия приложения

version: packageJson.version

Кэш сбрасывается после обновления приложения.


Хэш конфигурации

const crypto = require('crypto');

const configHash = crypto
    .createHash('md5')
    .update(JSON.stringify(customConfig))
    .digest('hex');

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

Позволяет автоматически инвалидировать кэш при изменении пользовательской конфигурации.


Комбинированная стратегия

version: [
    process.env.NODE_ENV,
    packageJson.version,
    customHash
].join('-')

Наиболее распространённый промышленный подход.


Параметр buildDependencies

Назначение buildDependencies

buildDependencies сообщает Webpack, какие файлы влияют на сборку помимо обычных модулей проекта.

Если один из этих файлов изменяется — кэш инвалидируется.

Пример:

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

Здесь __filename — текущий webpack.config.js.


Почему buildDependencies необходим

Webpack отслеживает зависимости модулей, но не всегда способен определить:

  • какие внешние файлы участвуют в конфигурации;
  • какие JSON-файлы влияют на loader’ы;
  • какие скрипты генерации влияют на сборку;
  • какие дополнительные настройки участвуют в pipeline.

Без buildDependencies Webpack может использовать устаревший кэш.


Структура buildDependencies

Параметр представляет объект:

buildDependencies: {
    имяГруппы: [массивФайлов]
}

Пример:

buildDependencies: {
    config: [
        __filename,
        path.resolve(__dirname, 'build/utils.js')
    ]
}

Имена групп (config) используются только для логической организации.


Отслеживание вспомогательных конфигураций

Часто webpack.config.js импортирует внешние файлы:

const aliases = require('./config/aliases');
const rules = require('./config/rules');

Webpack может не всегда корректно отслеживать такие зависимости в сложных сценариях.

Надёжный вариант:

buildDependencies: {
    config: [
        __filename,
        path.resolve(__dirname, 'config/aliases.js'),
        path.resolve(__dirname, 'config/rules.js')
    ]
}

Отслеживание tsconfig.json

TypeScript-конфигурация влияет на:

  • alias;
  • module resolution;
  • target;
  • jsx;
  • paths.

Поэтому tsconfig.json желательно добавлять в зависимости сборки.

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

Отслеживание Babel-конфигурации

buildDependencies: {
    babel: [
        path.resolve(__dirname, '.babelrc')
    ]
}

или:

buildDependencies: {
    babel: [
        path.resolve(__dirname, 'babel.config.js')
    ]
}

Отслеживание PostCSS-конфигурации

buildDependencies: {
    styles: [
        path.resolve(__dirname, 'postcss.config.js')
    ]
}

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

Webpack поддерживает отслеживание каталогов:

buildDependencies: {
    config: [
        path.resolve(__dirname, 'config/')
    ]
}

При изменении любого файла внутри директории кэш инвалидируется.

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


Разделение зависимостей по группам

Крупные проекты часто структурируют зависимости:

buildDependencies: {
    webpack: [
        __filename
    ],

    babel: [
        path.resolve(__dirname, 'babel.config.js')
    ],

    typescript: [
        path.resolve(__dirname, 'tsconfig.json')
    ],

    styles: [
        path.resolve(__dirname, 'postcss.config.js')
    ]
}

Это улучшает поддержку конфигурации.


Связь с loader’ами и plugin’ами

Многие plugin’ы и loader’ы используют собственные файлы конфигурации:

  • ESLint;
  • Babel;
  • PostCSS;
  • Stylelint;
  • SWC;
  • TypeScript;
  • GraphQL codegen;
  • SVG pipelines.

Все подобные файлы желательно включать в buildDependencies.


Автоматическая инвалидация webpack.config.js

Webpack автоматически добавляет текущий конфиг в зависимости сборки.

Но если конфигурация разбита на несколько файлов:

webpack/
├── common.js
├── prod.js
├── dev.js
└── loaders.js

необходимо явно указывать дополнительные зависимости.


Параметр name

Назначение name

cache.name задаёт имя кэша.

Webpack использует его для разделения нескольких независимых кэшей.

Пример:

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

Зачем нужен name

Без отдельного имени разные сборки могут конфликтовать:

  • client/server;
  • development/production;
  • SSR/browser;
  • multiple targets;
  • multi-compiler mode.

Разделение кэшей предотвращает:

  • повреждение кэша;
  • использование неподходящих данных;
  • ложные cache hit;
  • некорректную оптимизацию.

Разделение production и development

cache: {
    type: 'filesystem',
    name: process.env.NODE_ENV
}

Теперь создаются разные кэши:

  • development
  • production

Разделение client и server

В SSR-проектах:

cache: {
    type: 'filesystem',
    name: 'client'
}

и отдельно:

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

Multi-compiler mode

Webpack поддерживает массив конфигураций:

module.exports = [
    clientConfig,
    serverConfig
];

Каждая конфигурация должна иметь собственный name.

Пример:

const clientConfig = {
    name: 'client',
    cache: {
        type: 'filesystem',
        name: 'client-cache'
    }
};

const serverConfig = {
    name: 'server',
    cache: {
        type: 'filesystem',
        name: 'server-cache'
    }
};

Связь name и cacheDirectory

Webpack формирует путь к кэшу на основе:

  • cacheDirectory
  • name

Пример:

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

Фактический путь будет примерно таким:

.webpack-cache/frontend

Динамические имена кэша

Иногда имя зависит от платформы:

name: `cache-${process.platform}`

или:

name: `cache-${process.env.NODE_ENV}`

Разделение кэша монорепозиториев

В monorepo:

packages/
├── admin/
├── storefront/
└── api/

каждый пакет может иметь собственный кэш:

cache: {
    type: 'filesystem',
    name: 'admin'
}

Совместное использование version, buildDependencies и name

На практике эти параметры работают совместно.

Типичная промышленная конфигурация:

const path = require('path');
const packageJson = require('./package.json');

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

        name: process.env.NODE_ENV,

        version: packageJson.version,

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

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

Отсутствие buildDependencies

Частая проблема:

cache: {
    type: 'filesystem'
}

При этом проект зависит от:

  • Babel;
  • PostCSS;
  • tsconfig;
  • alias-конфигураций.

В результате Webpack использует устаревший кэш.


Одинаковый name для разных сборок

Плохой вариант:

cache: {
    type: 'filesystem',
    name: 'default'
}

для всех конфигураций одновременно.

Это может приводить к конфликтам кэша.


Слишком частая смена version

Ошибка:

version: Date.now().toString()

Кэш будет инвалидироваться на каждой сборке.

Фактически кэширование перестаёт работать.


Слишком широкие buildDependencies

Плохой пример:

buildDependencies: {
    config: [
        __dirname
    ]
}

Любое изменение в проекте будет уничтожать кэш.


Игнорирование переменных окружения

Если сборка зависит от:

  • NODE_ENV;
  • feature flags;
  • API endpoints;
  • target platform;

но они не участвуют в version или name, возможны ошибки повторного использования кэша.


Диагностика проблем кэша

Очистка кэша

Иногда требуется ручной сброс:

rm -rf node_modules/.cache/webpack

Windows:

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

Проверка cache hit/miss

Полезен инфраструктурный логгер:

infrastructureLogging: {
    level: 'verbose'
}

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

  • использование кэша;
  • инвалидацию;
  • причины пересборки;
  • cache hit/miss.

Анализ нестабильной инвалидации

Если кэш постоянно пересоздаётся:

  • проверяется version;
  • проверяется динамика name;
  • анализируются buildDependencies;
  • исключаются случайные значения;
  • исключаются временные файлы.

Практическая схема промышленного проекта

Типичная архитектура:

const path = require('path');
const packageJson = require('./package.json');

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

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

        name: [
            process.env.NODE_ENV,
            process.platform
        ].join('-'),

        version: [
            packageJson.version,
            process.env.BUILD_VERSION
        ].join('-'),

        buildDependencies: {
            webpack: [
                __filename
            ],

            babel: [
                path.resolve(__dirname, 'babel.config.js')
            ],

            typescript: [
                path.resolve(__dirname, 'tsconfig.json')
            ],

            styles: [
                path.resolve(__dirname, 'postcss.config.js')
            ]
        }
    }
};

Такая конфигурация обеспечивает:

  • корректную инвалидацию;
  • стабильность кэша;
  • разделение окружений;
  • безопасную работу multi-build систем;
  • высокую скорость повторных сборок.