До появления полноценного файлового кэша в Webpack 5 проблема скорости повторных сборок оставалась одной из наиболее болезненных. Большие проекты с Babel, TypeScript, Sass, PostCSS и множеством зависимостей могли пересобираться десятки секунд даже после минимальных изменений.
Webpack 3 и Webpack 4 имели ограниченные встроенные механизмы оптимизации:
На этом фоне появился плагин HardSourceWebpackPlugin — один из самых известных экспериментов по реализации постоянного кэша компиляции.
Основная идея Hard Source заключалась в сохранении промежуточных результатов сборки на диск с последующим повторным использованием.
Плагин пытался кэшировать:
npm install --save-dev hard-source-webpack-plugin
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
};
Во время первой сборки Webpack выполнял полный цикл обработки:
Hard Source вмешивался во внутренние этапы compilation и сериализовал результаты на диск.
При следующей сборке:
В теории это позволяло сокращать время rebuild в несколько раз.
По умолчанию кэш сохранялся в:
node_modules/.cache/hard-source
Внутри находились:
.cache/
└── hard-source/
├── assets/
├── records/
├── module/
├── resolver/
└── stamp/
Разделение выполнялось по типам данных:
Плагин позволял переопределять расположение кэша.
new HardSourceWebpackPlugin({
cacheDirectory: 'node_modules/.cache/hard-source/[confighash]'
})
[confighash] использовался для разделения кэша между
разными конфигурациями.
Например:
Без разделения часто возникали конфликты между различными compilation pipeline.
Одной из основных проблем persistent cache являлась инвалидизация.
Hard Source пытался определить:
Для этого использовался environmentHash.
new HardSourceWebpackPlugin({
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock']
}
})
Плагин вычислял hash:
Если hash менялся — кэш считался невалидным.
Persistent cache в bundler’е значительно сложнее обычного файлового кэша.
Необходимо учитывать:
Даже минимальное изменение конфигурации могло приводить к некорректному восстановлению compilation state.
На крупных проектах Hard Source действительно давал заметный прирост.
Примерные показатели:
| Тип сборки | Без Hard Source | С Hard Source |
|---|---|---|
| Initial build | 45 сек | 47 сек |
| Rebuild | 38 сек | 8 сек |
| Incremental build | 25 сек | 5 сек |
Главное ускорение происходило именно при повторных сборках.
Главная причина отказа сообщества от Hard Source — нестабильность.
Плагин глубоко внедрялся во внутренности Webpack:
Webpack не предоставлял стабильного API для подобных операций.
После обновлений:
кэш часто становился повреждённым.
Иногда Webpack не замечал изменение файла.
Результат:
Некоторые loader’ы возвращали сложные структуры:
Hard Source не всегда мог корректно сериализовать подобные данные.
Особенно часто ломались:
Причина заключалась в огромном количестве промежуточных трансформаций AST.
Hot Module Replacement требовал точного отслеживания dependency graph.
Hard Source иногда восстанавливал устаревшие связи между модулями.
Это приводило к:
new HardSourceWebpackPlugin({
cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
environmentHash: {
root: process.cwd(),
directories: [],
files: [
'package-lock.json',
'yarn.lock'
]
},
configHash: function(webpackConfig) {
return require('node-object-hash')({
sort: false
}).hash(webpackConfig);
}
})
Подобные настройки были попыткой уменьшить вероятность некорректной инвалидизации.
На больших monorepo плагин часто вызывал:
Особенно проблемными были:
Webpack 5 внедрил встроенный persistent cache на уровне архитектуры bundler’а.
Это было принципиально важно.
Hard Source работал как внешний слой поверх Webpack.
Webpack 5 cache:
Современная замена Hard Source:
module.exports = {
cache: {
type: 'filesystem'
}
};
Дополнительные настройки:
module.exports = {
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
buildDependencies: {
config: [__filename]
}
}
};
| Возможность | Hard Source | Webpack 5 Cache |
|---|---|---|
| Интеграция в core | Нет | Да |
| Стабильность | Низкая | Высокая |
| Поддержка HMR | Частичная | Полная |
| Snapshot system | Нет | Да |
| File dependency tracking | Ограничено | Полноценное |
| Поддержка module graph | Частичная | Полная |
| Поддержка lazy compilation | Нет | Да |
| Совместимость с loader’ами | Проблемная | Значительно лучше |
Ещё одним популярным устаревшим подходом был cache-loader.
Loader сохранял результат предыдущего loader pipeline на диск.
Пример:
{
test: /\.js$/,
use: [
'cache-loader',
'babel-loader'
]
}
При первой обработке:
При повторной сборке:
cache-loader имел множество недостатков.
Не кэшировались:
На SSD прирост был заметен.
На HDD cache-loader иногда замедлял сборку из-за:
-大量 мелких файлов;
Проблемы часто возникали с:
babel-loader имел собственный механизм кэширования.
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
Babel сохранял:
Это решение оказалось значительно стабильнее Hard Source, поскольку кэшировался только этап Babel.
Ещё один подход к ускорению сборок — распараллеливание.
{
test: /\.js$/,
use: [
'thread-loader',
'babel-loader'
]
}
thread-loader запускал worker pool.
Проблемы:
Иногда thread-loader замедлял build вместо ускорения.
До появления persistent cache широко использовался DLLPlugin.
Vendor dependencies собирались отдельно:
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.join(__dirname, '[name]-manifest.json')
})
]
Затем основная сборка ссылалась на готовые vendor bundles.
Подход был сложным в поддержке:
Webpack 5 практически устранил необходимость DLL-сборок.
До thread-loader популярностью пользовался Happypack.
new HappyPack({
loaders: ['babel-loader']
})
Плагин распределял loader execution между worker processes.
Проблемы Happypack:
Во времена Webpack 4 экосистема ускорения сборки выглядела фрагментированной.
Типичный production stack мог содержать одновременно:
Поддержка подобной конфигурации была крайне сложной.
Практически все старые плагины пытались:
Но Webpack долгое время не проектировался как система с persistent incremental cache.
В результате:
Одним из важнейших нововведений стала snapshot system.
Webpack начал отслеживать:
Это позволило реализовать безопасную инвалидизацию кэша.
Webpack 5 научился различать:
Пример:
snapshot: {
managedPaths: [
/node_modules/
],
immutablePaths: []
}
Это резко уменьшило объём filesystem checks.
Современный Webpack уже содержит:
Большинство внешних решений:
В старых enterprise-проектах всё ещё можно встретить:
HardSourceWebpackPlugin
cache-loader
HappyPack
DLLPlugin
Особенно в:
Наиболее распространённая стратегия миграции:
npm remove hard-source-webpack-plugin
Замена:
cache: {
type: 'filesystem'
}
Было:
use: [
'cache-loader',
'babel-loader'
]
Стало:
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
Или полный переход на встроенный cache Webpack 5.
Современные replacement-механизмы:
Характерные симптомы:
.cache;Во многих случаях причиной оказываются именно устаревшие механизмы кэширования.