Симптом: после добавления admin-entry вырос site.[contenthash].js, а Network обычной страницы показывает запрос к admin.[contenthash].js. Пользователь получает код панели, которой не откроет; если править только сумму файлов в dist, легко оставить этот лишний запрос или сломать подключение нужного entry. Цена ошибки — лишний байт в критическом пути и неверная загрузка административного кода.
Главный вопрос статьи: как по данным Webpack 4 доказать, почему bundle вырос после добавления entry, прежде чем менять конфигурацию? Для ответа нужны три вещи: список emitted-ассетов, связь модуля с chunks и фактические script-теги в HTML. Одной цифры из файловой системы недостаточно.
Сначала фиксирую условия сравнения
Сравнивать development-результат с production-результатом бессмысленно: режим, минификация, source map и плагины меняют картину сильнее, чем новый entry. Я делаю два production-build на одном коммите: до изменения и после него. Для каждого сохраняю JSON статистики отдельно, например stats-before.json и stats-after.json.
Создаю stats.json из той же команды сборки
Webpack умеет отдать статистику компиляции в JSON. В ней есть ассеты, chunks, модули и их связи. Команду лучше запускать локальным webpack-cli из проекта: тогда версия сборщика совпадает с той, для которой написан webpack.config.js.
# В package.json уже есть webpack и webpack-cli.
./node_modules/.bin/webpack --mode production --profile --json > stats-after.json
# Для второго снимка возвращаем только конфигурацию entry
# и повторяем ту же команду:
./node_modules/.bin/webpack --mode production --profile --json > stats-before.json
Параметр --profile добавляет сведения о времени по модулям. Для вопроса о размере он не обязателен, но снимок пригодится, если рост размера сопровождается долгой сборкой. Главное — не смешивать JSON со случайными console.log из конфигурации: файл должен остаться валидным JSON.
Читаю сначала ассеты, а не весь граф
Первый разрез простой: сортирую emitted-ассеты по size. Это показывает, какие выходные файлы появились и какие из них стали больше. Но размер в stats — размер ассета в сборке, а не обязательно число байтов, переданных по сети после gzip или кеширования. Поэтому это место для гипотезы, а не для вывода о скорости страницы.
// tools/print-webpack-stats.js
const fs = require('fs');
const stats = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const assets = (stats.assets || [])
.map((asset) => ({
name: asset.name,
size: asset.size,
chunks: asset.chunks || [],
}))
.sort((left, right) => right.size - left.size);
for (const asset of assets) {
console.log(asset.size + '\\t' + asset.name + '\\tchunks=' + asset.chunks.join(','));
}
const repeated = (stats.modules || [])
.filter((module) => Array.isArray(module.chunks) && module.chunks.length > 1)
.map((module) => ({
name: module.name,
chunks: module.chunks,
size: module.size,
}))
.sort((left, right) => right.size - left.size);
console.log('\\nModules present in more than one chunk:');
for (const module of repeated.slice(0, 30)) {
console.log(module.size + '\\t' + module.name + '\\tchunks=' + module.chunks.join(','));
}
Запуск node tools/print-webpack-stats.js stats-after.json не должен автоматически объявлять все строки из второго списка проблемой. Общий модуль может быть правильно связан с несколькими chunks в описании компиляции, а часть chunks может быть асинхронной. Список нужен, чтобы назвать конкретный модуль, после чего его надо сопоставить с entrypoint и HTML.
| Наблюдение | Что это может означать | Следующее действие |
|---|---|---|
Появился новый admin.[contenthash].js, старый site.[contenthash].js почти не изменился | В dist лежит ещё одна страница, но старая не стала тяжелее | Проверить, что старый HTML не подключает admin |
Один пакет из node_modules виден у двух initial chunks | Внешняя зависимость достигнута из двух entry и не вынесена | Проверить splitChunks и условия cache group |
| Оба entry подключены в одном HTML | Шаблон страницы получает чужой сценарий | Исправить генерацию script-тегов до настройки оптимизации |
| Рост только в development | Сравнение сделано в разных режимах или с source map | Повторить замер одинаковой production-командой |
| Файл большой в stats, но не запрашивается на странице | Ассет существует, но не входит в нужный entrypoint | Смотреть Network для конкретного URL, а не сумму каталога |
Проверяю entrypoint и сетевой след
В stats есть сведения о chunks и entrypoints. Если новый admin должен жить только на /admin/, я открываю обычную страницу и смотрю список скриптов в HTML и вкладку Network. На ней должны быть только runtime, общие chunks, нужные именно этой странице, и её entry. Если там уже есть admin, проблема находится в шаблоне или плагине, а не в размере модуля.
Затем повторяю проверку для /admin/. Только когда один и тот же большой модуль действительно участвует в двух начальных путях, есть смысл добавлять cache group. В Webpack 4 оптимизация общих chunks по умолчанию ориентирована на динамические imports; для начальных chunks нужно явно выбрать подходящую конфигурацию. Это объясняет, почему «поставил второй entry» и «получил отдельный общий файл» не равны друг другу.
Небольшая правка после доказательства
Когда stats показал повторяющийся пакет, а обе страницы действительно его загружают, я добавляю минимальную группу, а не копирую чужой длинный конфиг. Сначала отделяю пакеты из node_modules. Общий код приложения стоит выносить отдельным правилом только после того, как видно повтор из двух entry и он достаточно велик для отдельного запроса.
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single',
}
После изменения я создаю третий stats-fixed.json и повторяю те же три проверки. Ожидаемый результат формулирую не как «стало мало килобайт», а как наблюдаемый контракт: обычная страница не загружает admin-entry; общий пакет появился в предназначенном для него chunk; обе страницы получают все необходимые файлы без ошибки выполнения.
Последовательность расследования
- Сохранить два stats-снимка из одинаковой production-команды и назвать версии webpack и webpack-cli.
- Сравнить ассеты: какой файл вырос, какой появился, связан ли он с новым entry.
- Найти крупные модули, отмеченные в нескольких chunks, и не путать этот сигнал с доказательством сетевой загрузки.
- Открыть каждый HTML-маршрут с пустым кешем и проверить реальные script-теги и Network.
- Только после подтверждения дублирования настроить одну cache group, пересобрать и повторить тот же снимок.
Ограничения метода
Stats JSON отражает конкретную версию Webpack 4 и состав компиляции. Названия полей и формат данных могут меняться после обновления сборщика, поэтому диагностический скрипт не стоит превращать в вечный CI-контракт без фиксации версии. Метод также не измеряет время первой отрисовки и не учитывает серверное сжатие; для этого нужен отдельный сетевой замер. Но он надёжно отделяет «в каталоге стало больше файлов» от конкретного вопроса «какой модуль попал в какой chunk и почему».
Итог
Новый entry сам по себе увеличивает число ассетов — это ожидаемо. Дублирование начинается не от количества файлов, а от повторно достижимого модуля и от того, какие chunks подключает HTML. stats.json даёт материал для первой части проверки, браузер — для второй. После такой пары доказательств настройка splitChunks становится короткой и объяснимой.