После добавления admin.js в конфигурацию и site.js, и admin.js могут содержать date-format.js. Руки тянутся перенести модуль в отдельную папку или добавить третий entry с названием vendor. Это не объясняет причину. Файл уже общий на диске; проблема возникает позже, когда Webpack строит стартовые графы. Цена ошибки — лишний код в каждом entry и увеличение загрузки страницы.
Главный вопрос здесь один: почему один и тот же import попадает в два entry bundle до настройки общего chunk? Разобрав этот механизм, можно отличить две настоящие страницы от одного entry с подготовительными файлами и не превратить библиотеку в фальшивую точку запуска.
Entry не равен bundle, но задаёт его начало
Webpack начинает с entry и рекурсивно проходит import и require. Результатом становится граф зависимостей. При одном entry у графа один старт. При объекте из site и admin — два старта. Если оба пути доходят до одного модуля, сам модуль остаётся одним исходным файлом, но без дополнительного правила может оказаться в обоих начальных chunks.
Минимальный граф, который показывает проблему
// src/site.js
import { formatDate } from './shared/date-format';
import { mountSearch } from './site/search';
mountSearch(formatDate);
// src/admin.js
import { formatDate } from './shared/date-format';
import { mountReport } from './admin/report';
mountReport(formatDate);
// src/shared/date-format.js
export function formatDate(date) {
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
}
В этом примере site/search и admin/report принадлежат разным страницам. shared/date-format достижим из обеих. Это полезная граница: переносить search в общий chunk ради симметрии не нужно; он не нужен админке. А date-format можно рассматривать как кандидата на общий chunk, если цена дополнительного файла оправдана.
Запись в entry | Сколько стартов выполнения | Когда использовать |
|---|---|---|
"./src/site.js" | Один | Одна страница или библиотека с одним началом |
["./src/polyfills.js", "./src/site.js"] | Один | Нужно выполнить подготовительный файл перед главным кодом той же страницы |
{ site: "./src/site.js", admin: "./src/admin.js" } | Два | Сервер выдаёт два независимых HTML-документа |
{ vendor: ["jquery"], site: "./src/site.js" } | Два, один из них фиктивный | Для Webpack 4 это плохая модель; общий код выделяет splitChunks |
Почему массив не создаёт вторую страницу
Массив в entry имеет другой смысл: Webpack 4 собирает указанные файлы как один multi-main entry и обходит их зависимости в одном chunk. Это подходит для полифиллов или кода подготовки, который всегда должен выполниться перед приложением. Массив не создаёт отдельную страницу и не заменяет объектную запись для многостраничного сайта.
// Один entry: polyfills и сайт попадают в один стартовый граф.
entry: ['./src/polyfills.js', './src/site.js']
// Два entry: сервер обязан отдать нужный набор файлов каждой странице.
entry: {
site: './src/site.js',
admin: './src/admin.js',
}
Где Webpack 4 разделяет общий участок
До Webpack 4 встречалась привычка писать отдельный entry для библиотек и подключать CommonsChunkPlugin. В документации Webpack 4 этот путь уже помечен как нежелательный: entry должен соответствовать старту выполнения, а внешний и общий код выделяет optimization.splitChunks. Правило не обещает, что любой общий модуль обязательно станет отдельным файлом: на результат влияют условия группы, размер и тип chunk.
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
common: {
name: 'common',
minChunks: 2,
minSize: 0,
chunks: 'all',
},
},
},
}
Здесь я снова ставлю minSize: 0 только для наглядности. Условия говорят: найти модуль, использованный хотя бы в двух chunks, и вынести его в файл common. В production-конфигурации сначала стоит снять реальные размеры, а потом вернуть порог, который не дробит приложение на множество мелких файлов.
Runtime — соседняя, но другая деталь
После выделения общего модуля иногда кажется, что дублирование осталось: в каждом entry виден служебный код Webpack. У runtime отдельная роль — он знает, как загружать модули и chunks. В Webpack 4 по умолчанию runtime встроен в entry; runtimeChunk: "single" создаёт один общий runtime-файл. Это решение полезно для нескольких страниц, но не нужно путать его с переносом общего прикладного модуля.
Если одна HTML-страница всё же включает несколько entry, у неё есть дополнительный риск: документация Webpack 4 предупреждает, что импортированные модули инициализируются для каждого runtime отдельно. Поэтому два script-тега не являются нейтральным приёмом. Сначала стоит проверить, нельзя ли оставить один старт и сделать вторую часть модулем внутри него.
Порядок проверки графа
- Выписать HTML-документы и ответить, нужен ли каждому отдельный старт JavaScript.
- Проверить, не является ли список файлов в массиве одним entry, где второй файл нужен только для подготовки.
- Найти модуль, который достигается от двух независимых стартов, и не выносить в общий код модули, нужные одной странице.
- Настроить
splitChunksна маленьком примере, затем вернуть проектный порог размера. - Отдельно решить, нужен ли единый runtime, и проверить фактические script-теги каждой страницы.
Граница объяснения
Эта модель отвечает только на вопрос о начальных chunks. Она не говорит, что нужно вынести каждый импорт, и не заменяет анализ загрузки по действию пользователя. Модуль для редкого окна или отчёта может быть лучше загрузить через динамический import(). Ещё важно помнить о версии: конфигурация и названия опций в тексте относятся к Webpack 4; пример из свежей документации с новыми полями entry нельзя без проверки вставлять в старый проект.
Итог
Один import попадает в два entry bundle не потому, что файл лежит «не в той папке». Он достижим из двух стартов. Сначала нужно назвать эти старты, затем решить судьбу общего участка графа через splitChunks и только потом смотреть на размер файлов. Такой порядок оставляет в конфигурации причину, а не случайную заплатку.