DarkRiDDeR10 мин

Webpack 4. Почему общий import оказывается в двух entry bundle

JavaScriptWebpack

После добавления 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.

Граф Webpack 4: entry site и admin проходят к своим модулям и оба достигают shared/date-format и jquery; до splitChunks общие зависимости могут присутствовать в обоих стартовых 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-тега не являются нейтральным приёмом. Сначала стоит проверить, нельзя ли оставить один старт и сделать вторую часть модулем внутри него.

Порядок проверки графа

  1. Выписать HTML-документы и ответить, нужен ли каждому отдельный старт JavaScript.
  2. Проверить, не является ли список файлов в массиве одним entry, где второй файл нужен только для подготовки.
  3. Найти модуль, который достигается от двух независимых стартов, и не выносить в общий код модули, нужные одной странице.
  4. Настроить splitChunks на маленьком примере, затем вернуть проектный порог размера.
  5. Отдельно решить, нужен ли единый runtime, и проверить фактические script-теги каждой страницы.

Граница объяснения

Эта модель отвечает только на вопрос о начальных chunks. Она не говорит, что нужно вынести каждый импорт, и не заменяет анализ загрузки по действию пользователя. Модуль для редкого окна или отчёта может быть лучше загрузить через динамический import(). Ещё важно помнить о версии: конфигурация и названия опций в тексте относятся к Webpack 4; пример из свежей документации с новыми полями entry нельзя без проверки вставлять в старый проект.

Итог

Один import попадает в два entry bundle не потому, что файл лежит «не в той папке». Он достижим из двух стартов. Сначала нужно назвать эти старты, затем решить судьбу общего участка графа через splitChunks и только потом смотреть на размер файлов. Такой порядок оставляет в конфигурации причину, а не случайную заплатку.

Проверяемые источники