Кэш резолвера и кэш загрузчиков

Система кэширования в Webpack предназначена для сокращения времени повторных сборок. Во время компиляции Webpack выполняет множество дорогостоящих операций:

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

Без кэширования каждая сборка повторяла бы все этапы заново, даже если исходный код почти не изменился. Особенно заметна проблема в крупных проектах с тысячами модулей и большим количеством loader-цепочек.

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

  • кэш резолвера;
  • кэш загрузчиков;
  • memory cache;
  • filesystem cache;
  • persistent cache;
  • внутренние кэши модулей и чанков.

Кэш резолвера и кэш загрузчиков относятся к наиболее важным механизмам ускорения сборки.


Кэш резолвера

Назначение resolver cache

Resolver в Webpack отвечает за поиск и определение фактического пути модуля. Когда встречается импорт:

import Button from '@/components/Button';

Webpack выполняет длинную последовательность действий:

  1. Анализирует alias.
  2. Проверяет extensions.
  3. Ищет package.json.
  4. Проверяет exports/imports.
  5. Выполняет обход node_modules.
  6. Проверяет symlink.
  7. Производит обращения к файловой системе.

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

Resolver cache сохраняет результаты разрешения путей, чтобы повторно не выполнять одинаковые вычисления.


Как работает резолвер

Пример процесса resolve

Для импорта:

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"
  }
}

резолвер анализирует структуру пакета.

Каждая такая операция кэшируется.


enhanced-resolve

Webpack использует библиотеку:

enhanced-resolve

Она реализует:

  • алгоритмы поиска модулей;
  • обработку alias;
  • extensions;
  • symlink;
  • conditionNames;
  • exports/imports;
  • plugins резолвера.

Кэш находится именно внутри enhanced-resolve.


Что попадает в кэш резолвера

Обычно кэшируются:

  • resolved path;
  • информация о package.json;
  • результаты alias;
  • directory existence checks;
  • file existence checks;
  • symlink resolution;
  • exports resolution;
  • extension resolution.

Пример:

import React from 'react';

После первого resolve результат может быть сохранён:

/react-project/node_modules/react/index.js

Следующие импорты React используют уже кэшированный результат.


cacheWithContext

Опция resolve.cacheWithContext

Настройка:

resolve: {
    cacheWithContext: false
}

управляет зависимостью кэша от context.

Значение true

При true Webpack учитывает:

  • issuer;
  • context directory;
  • environment;
  • дополнительные параметры.

Кэш становится более точным, но менее переиспользуемым.


Значение false

При false кэш более агрессивный.

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

  • меньше записей;
  • выше hit rate;
  • быстрее resolve.

Недостаток — некоторые edge-case сценарии могут разрешаться некорректно при сложной конфигурации.


resolve.cache

В старых версиях Webpack использовалась настройка:

resolve: {
    cache: true
}

В Webpack 5 механизм кэширования значительно переработан и интегрирован с общей системой persistent cache.


Snapshot-система

Связь с resolver cache

Webpack 5 использует snapshot-механизм.

Snapshot хранит информацию о:

  • timestamps;
  • hashes;
  • состоянии файлов;
  • директориях;
  • package metadata.

Если snapshot показывает отсутствие изменений, resolver cache считается валидным.


managedPaths

Управляемые пути

Настройка:

snapshot: {
    managedPaths: [
        /node_modules/
    ]
}

говорит Webpack, что директории управляются package manager.

Это позволяет:

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

immutablePaths

Неизменяемые директории

Пример:

snapshot: {
    immutablePaths: [
        path.resolve(__dirname, '.yarn/cache')
    ]
}

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

Это особенно эффективно для:

  • Yarn PnP;
  • pnpm store;
  • CI environments.

Resolver cache и alias

Кэширование alias

Конфигурация:

resolve: {
    alias: {
        '@components': path.resolve(__dirname, 'src/components')
    }
}

После первого разрешения:

@components/Button

Webpack сохраняет конечный путь.

Без кэша каждый импорт снова запускал бы цепочку alias resolution.


Resolver cache и extensions

Проверка расширений

Конфигурация:

resolve: {
    extensions: ['.js', '.ts', '.jsx']
}

Для каждого import Webpack перебирает расширения.

Например:

Button
Button.js
Button.ts
Button.jsx

Результат проверки сохраняется в resolver cache.


Resolver cache и symlink

Разрешение символьных ссылок

Monorepo-проекты часто используют symlink:

packages/
shared/
ui/

Webpack выполняет:

realpath()

Эта операция дорогая для файловой системы.

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


Resolver cache и package exports

Package exports resolution

Современные пакеты используют:

{
  "exports": {
    ".": {
      "import": "./esm/index.js",
      "require": "./cjs/index.js"
    }
  }
}

Webpack анализирует:

  • тип импорта;
  • conditionNames;
  • target environment.

Результаты кэшируются отдельно.


Кэш загрузчиков

Loader cache

Loader cache сохраняет результат работы загрузчиков.

Без него каждый loader выполнялся бы заново:

  • Babel;
  • TypeScript;
  • Sass;
  • PostCSS;
  • SWC;
  • ESLint loader.

Это одна из самых затратных частей сборки.


Как работают загрузчики

Пример цепочки:

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

Webpack:

  1. Читает файл.
  2. Передаёт код в loader.
  3. Loader трансформирует код.
  4. Возвращает результат.

Для Babel трансформация может занимать десятки миллисекунд на файл.

В проекте с тысячами модулей время становится огромным.


babel-loader cache

cacheDirectory

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

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

Babel сохраняет результат трансформации в:

node_modules/.cache/babel-loader

Повторная сборка использует готовый JS-код вместо повторной компиляции.


Как работает cacheDirectory

Для каждого файла вычисляется cache key.

В ключ входят:

  • исходный код;
  • babel config;
  • env;
  • версия Babel;
  • версия loader;
  • options;
  • target browsers.

Если ключ совпадает — результат берётся из кэша.


cacheCompression

Сжатие кэша

Опция:

options: {
    cacheDirectory: true,
    cacheCompression: false
}

По умолчанию Babel может gzip-сжимать кэш.

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

  • меньше размер на диске.

Недостатки:

  • CPU overhead;
  • дополнительное время на compress/decompress.

На SSD и быстрых CI часто выгоднее отключать compression.


cacheIdentifier

Идентификатор кэша

Пример:

options: {
    cacheIdentifier: 'custom-cache-v1'
}

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

Полезно при:

  • изменении internal tooling;
  • обновлении presets;
  • миграции конфигурации.

thread-loader и кэш

Совместная работа

Конфигурация:

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

thread-loader распределяет работу между worker-потоками.

Кэш loader’ов уменьшает объём работы внутри workers.

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


ts-loader cache

TypeScript loader cache

Пример:

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

TypeScript имеет собственные механизмы incremental build:

{
  "compilerOptions": {
    "incremental": true
  }
}

Webpack может использовать:

  • filesystem cache;
  • tsbuildinfo;
  • loader cache.

Совместное использование резко ускоряет rebuild.


sass-loader cache

Кэширование Sass

Sass-компиляция может быть дорогой из-за:

  • nested imports;
  • calculations;
  • mixins;
  • maps;
  • large design systems.

При filesystem cache Webpack сохраняет результаты преобразования CSS.


postcss-loader cache

PostCSS выполняет:

  • autoprefixer;
  • nesting;
  • minification;
  • custom plugins.

Каждый plugin требует AST traversal.

Кэширование существенно уменьшает rebuild time.


Встроенный filesystem cache

Webpack 5 cache.type

Webpack 5 ввёл встроенный persistent cache:

cache: {
    type: 'filesystem'
}

Теперь Webpack может кэшировать:

  • результаты loader’ов;
  • модули;
  • resolve operations;
  • generated assets.

Структура filesystem cache

Webpack сохраняет данные в:

node_modules/.cache/webpack

Там находятся:

  • serialized modules;
  • snapshots;
  • dependency graphs;
  • loader results;
  • resolve cache.

buildDependencies

Инвалидация конфигурации

Пример:

cache: {
    type: 'filesystem',
    buildDependencies: {
        config: [__filename]
    }
}

Webpack отслеживает изменения:

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

При изменении кэш сбрасывается.


Loader cache invalidation

Когда кэш становится невалидным

Кэш загрузчиков инвалидируется при изменении:

  • исходного файла;
  • loader options;
  • loader version;
  • webpack version;
  • environment variables;
  • babel config;
  • tsconfig;
  • postcss config.

Deterministic cache keys

Стабильность ключей

Webpack 5 использует deterministic serialization.

Это уменьшает:

  • ложные invalidation;
  • нестабильность rebuild;
  • проблемы CI cache reuse.

Memory cache

Кэш в памяти

Во время watch mode Webpack активно использует memory cache.

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

  • мгновенный доступ;
  • отсутствие disk IO;
  • быстрые rebuild.

Недостаток — кэш исчезает после завершения процесса.


Persistent cache

Постоянный кэш

Filesystem cache сохраняется между запусками:

npm run build
npm run build

Вторая сборка значительно быстрее.

Особенно заметно:

  • в CI;
  • Docker;
  • monorepo;
  • large enterprise projects.

Idle timeout

Очистка кэша

Настройка:

cache: {
    type: 'filesystem',
    idleTimeout: 60000
}

Управляет временем ожидания перед сохранением кэша на диск.


maxMemoryGenerations

Ограничение memory cache

Пример:

cache: {
    maxMemoryGenerations: 5
}

Управляет количеством поколений объектов в памяти.

Помогает контролировать RAM usage.


Experiments.cacheUnaffected

Экспериментальная оптимизация

Пример:

experiments: {
    cacheUnaffected: true
}

Webpack пытается переиспользовать неизменённые части графа модулей.

Эффективно для:

  • large SPA;
  • monorepo;
  • incremental rebuild.

Lazy compilation и кэш

Совместная работа

При:

experiments: {
    lazyCompilation: true
}

Webpack компилирует модули только по запросу.

Кэш уменьшает стоимость последующих обращений.


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

Кэш в CI

Обычно сохраняются директории:

node_modules/.cache

или:

node_modules/.cache/webpack

Это позволяет:

  • ускорять pipeline;
  • уменьшать build time;
  • снижать нагрузку на runners.

Проблемы устаревшего кэша

Cache poisoning

Иногда кэш становится неконсистентным:

  • после обновления loader;
  • при смене Node.js;
  • при изменении env;
  • после повреждения snapshot.

Симптомы:

  • неожиданные ошибки;
  • старый код в бандле;
  • некорректные sourcemaps.

Очистка кэша

Ручное удаление

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

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’ы плохо подходят для кэша:

  • нестабильные transforms;
  • генерация случайных значений;
  • зависимости от network state;
  • зависимости от system time.

Такие операции нарушают deterministic behavior.


Кэширование и monorepo

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

В monorepo количество модулей огромно:

packages/
apps/
shared/
tools/

Resolver cache особенно важен из-за:

  • symlink;
  • workspace dependencies;
  • package exports;
  • alias chains.

Filesystem cache позволяет dramatically уменьшить rebuild time.


Yarn PnP и resolver cache

Plug’n’Play

Yarn PnP исключает node_modules.

Webpack использует специальный resolver.

Кэширование становится ещё важнее, поскольку:

  • resolution logic сложнее;
  • используется virtual filesystem;
  • package lookup дороже.

Profiling кэша

Анализ эффективности

Webpack profiling позволяет определить:

  • cache hit rate;
  • expensive loaders;
  • slow resolve operations;
  • invalidation frequency.

Полезные инструменты:

  • SpeedMeasurePlugin;
  • webpack profiling;
  • stats.json;
  • infrastructureLogging.

infrastructureLogging

Диагностика кэша

Пример:

infrastructureLogging: {
    level: 'verbose'
}

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

  • cache hits;
  • cache misses;
  • snapshot invalidation;
  • rebuild причины.

Это помогает анализировать производительность сборки.


Лучшая стратегия кэширования

Наиболее эффективная комбинация для современных проектов:

cache: {
    type: 'filesystem'
}

вместе с:

  • babel-loader cacheDirectory;
  • snapshot.managedPaths;
  • thread-loader;
  • deterministic module ids;
  • persistent cache;
  • корректной invalidation strategy.

Именно сочетание resolver cache и loader cache обеспечивает основное ускорение incremental-сборок в Webpack 5.