Система кэширования в Webpack предназначена для сокращения времени повторных сборок. Во время компиляции Webpack выполняет множество дорогостоящих операций:
Без кэширования каждая сборка повторяла бы все этапы заново, даже если исходный код почти не изменился. Особенно заметна проблема в крупных проектах с тысячами модулей и большим количеством loader-цепочек.
Webpack использует несколько уровней кэша:
Кэш резолвера и кэш загрузчиков относятся к наиболее важным механизмам ускорения сборки.
Resolver в Webpack отвечает за поиск и определение фактического пути модуля. Когда встречается импорт:
import Button from '@/components/Button';
Webpack выполняет длинную последовательность действий:
Каждая операция требует чтения диска и множества системных вызовов. В крупном проекте количество операций разрешения модулей может измеряться десятками тысяч.
Resolver cache сохраняет результаты разрешения путей, чтобы повторно не выполнять одинаковые вычисления.
Для импорта:
import api from './services/api';
Webpack может проверить:
./services/api
./services/api.js
./services/api.json
./services/api.ts
./services/api/index.js
Если используются alias:
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
то выполняется дополнительная трансформация пути.
При использовании package exports:
{
"exports": {
".": "./dist/index.js"
}
}
резолвер анализирует структуру пакета.
Каждая такая операция кэшируется.
Webpack использует библиотеку:
enhanced-resolve
Она реализует:
Кэш находится именно внутри enhanced-resolve.
Обычно кэшируются:
Пример:
import React from 'react';
После первого resolve результат может быть сохранён:
/react-project/node_modules/react/index.js
Следующие импорты React используют уже кэшированный результат.
Настройка:
resolve: {
cacheWithContext: false
}
управляет зависимостью кэша от context.
При true Webpack учитывает:
Кэш становится более точным, но менее переиспользуемым.
При false кэш более агрессивный.
Преимущества:
Недостаток — некоторые edge-case сценарии могут разрешаться некорректно при сложной конфигурации.
В старых версиях Webpack использовалась настройка:
resolve: {
cache: true
}
В Webpack 5 механизм кэширования значительно переработан и интегрирован с общей системой persistent cache.
Webpack 5 использует snapshot-механизм.
Snapshot хранит информацию о:
Если snapshot показывает отсутствие изменений, resolver cache считается валидным.
Настройка:
snapshot: {
managedPaths: [
/node_modules/
]
}
говорит Webpack, что директории управляются package manager.
Это позволяет:
Пример:
snapshot: {
immutablePaths: [
path.resolve(__dirname, '.yarn/cache')
]
}
Webpack предполагает, что содержимое никогда не меняется.
Это особенно эффективно для:
Конфигурация:
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components')
}
}
После первого разрешения:
@components/Button
Webpack сохраняет конечный путь.
Без кэша каждый импорт снова запускал бы цепочку alias resolution.
Конфигурация:
resolve: {
extensions: ['.js', '.ts', '.jsx']
}
Для каждого import Webpack перебирает расширения.
Например:
Button
Button.js
Button.ts
Button.jsx
Результат проверки сохраняется в resolver cache.
Monorepo-проекты часто используют symlink:
packages/
shared/
ui/
Webpack выполняет:
realpath()
Эта операция дорогая для файловой системы.
Resolver cache уменьшает количество подобных вызовов.
Современные пакеты используют:
{
"exports": {
".": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
}
}
}
Webpack анализирует:
Результаты кэшируются отдельно.
Loader cache сохраняет результат работы загрузчиков.
Без него каждый loader выполнялся бы заново:
Это одна из самых затратных частей сборки.
Пример цепочки:
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
]
}
]
}
Webpack:
Для Babel трансформация может занимать десятки миллисекунд на файл.
В проекте с тысячами модулей время становится огромным.
Наиболее известный пример:
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
Babel сохраняет результат трансформации в:
node_modules/.cache/babel-loader
Повторная сборка использует готовый JS-код вместо повторной компиляции.
Для каждого файла вычисляется cache key.
В ключ входят:
Если ключ совпадает — результат берётся из кэша.
Опция:
options: {
cacheDirectory: true,
cacheCompression: false
}
По умолчанию Babel может gzip-сжимать кэш.
Преимущества:
Недостатки:
На SSD и быстрых CI часто выгоднее отключать compression.
Пример:
options: {
cacheIdentifier: 'custom-cache-v1'
}
Позволяет вручную инвалидировать кэш.
Полезно при:
Конфигурация:
use: [
'thread-loader',
'babel-loader'
]
thread-loader распределяет работу между worker-потоками.
Кэш loader’ов уменьшает объём работы внутри workers.
Без кэша многопоточность не всегда компенсирует стоимость компиляции.
Пример:
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
TypeScript имеет собственные механизмы incremental build:
{
"compilerOptions": {
"incremental": true
}
}
Webpack может использовать:
Совместное использование резко ускоряет rebuild.
Sass-компиляция может быть дорогой из-за:
При filesystem cache Webpack сохраняет результаты преобразования CSS.
PostCSS выполняет:
Каждый plugin требует AST traversal.
Кэширование существенно уменьшает rebuild time.
Webpack 5 ввёл встроенный persistent cache:
cache: {
type: 'filesystem'
}
Теперь Webpack может кэшировать:
Webpack сохраняет данные в:
node_modules/.cache/webpack
Там находятся:
Пример:
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
Webpack отслеживает изменения:
При изменении кэш сбрасывается.
Кэш загрузчиков инвалидируется при изменении:
Webpack 5 использует deterministic serialization.
Это уменьшает:
Во время watch mode Webpack активно использует memory cache.
Преимущества:
Недостаток — кэш исчезает после завершения процесса.
Filesystem cache сохраняется между запусками:
npm run build
npm run build
Вторая сборка значительно быстрее.
Особенно заметно:
Настройка:
cache: {
type: 'filesystem',
idleTimeout: 60000
}
Управляет временем ожидания перед сохранением кэша на диск.
Пример:
cache: {
maxMemoryGenerations: 5
}
Управляет количеством поколений объектов в памяти.
Помогает контролировать RAM usage.
Пример:
experiments: {
cacheUnaffected: true
}
Webpack пытается переиспользовать неизменённые части графа модулей.
Эффективно для:
При:
experiments: {
lazyCompilation: true
}
Webpack компилирует модули только по запросу.
Кэш уменьшает стоимость последующих обращений.
Обычно сохраняются директории:
node_modules/.cache
или:
node_modules/.cache/webpack
Это позволяет:
Иногда кэш становится неконсистентным:
Симптомы:
Наиболее распространённый способ:
rm -rf node_modules/.cache
или:
rm -rf node_modules/.cache/webpack
После этого Webpack создаёт кэш заново.
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
},
snapshot: {
managedPaths: [
/node_modules/
]
},
resolve: {
cacheWithContext: false,
extensions: ['.js', '.ts']
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}
}
]
}
};
Без кэша:
Initial build: 45s
Rebuild: 18s
С filesystem cache:
Initial build: 47s
Rebuild: 2s
В больших проектах выигрыш может быть ещё значительнее.
Некоторые loader’ы плохо подходят для кэша:
Такие операции нарушают deterministic behavior.
В monorepo количество модулей огромно:
packages/
apps/
shared/
tools/
Resolver cache особенно важен из-за:
Filesystem cache позволяет dramatically уменьшить rebuild time.
Yarn PnP исключает node_modules.
Webpack использует специальный resolver.
Кэширование становится ещё важнее, поскольку:
Webpack profiling позволяет определить:
Полезные инструменты:
Пример:
infrastructureLogging: {
level: 'verbose'
}
Webpack начинает выводить:
Это помогает анализировать производительность сборки.
Наиболее эффективная комбинация для современных проектов:
cache: {
type: 'filesystem'
}
вместе с:
Именно сочетание resolver cache и loader cache обеспечивает основное ускорение incremental-сборок в Webpack 5.