{ "index": 344, "slug": "editorial-2018-06-mechanism-webpack-entry", "title": "Webpack 4: почему общий модуль попадает в два entry bundle", "excerpt": "После добавления второго entry общий модуль может оказаться в обоих стартовых bundle. Разбираем граф зависимостей, смысл массива entry, splitChunks и проверку результата через stats и HTML.", "contentHtml": "
После добавления admin.js production-сборка начинает отдавать два больших стартовых файла. В обоих находится date-format.js. Иногда обычная страница ещё и загружает скрипт админки. Цена ошибки — лишние байты в критическом пути, второй runtime и код, который браузер скачивает, но не выполняет. Если исправить только имя файла или перенести модуль в другую папку, причина останется.
Тезис: Webpack строит граф от каждого entry. Один и тот же модуль попадает в два начальных bundle, когда оба графа до него доходят. Место файла в репозитории не определяет границу bundle. Сначала нужно установить реальные точки запуска. Затем — решить, какие общие части выделить через optimization.splitChunks, а какие оставить в странице или загрузить позже.
Entry задаёт начало обхода. Webpack читает entry, проходит его import и require, затем повторяет обход для найденных модулей и ассетов. Так появляется внутренний граф зависимостей. Output-файл — только один из результатов этого графа.
Объект с ключами site и admin означает два самостоятельных старта. Обычно это верно для двух HTML-документов: сервер отдаёт каталог с одним сценарием и панель с другим. Если один HTML подключает оба entry, конфигурация описывает не две страницы, а два старта внутри одной страницы. Это отдельная ошибка.
// src/site.js\nimport { formatDate } from './shared/date-format';\nimport { mountSearch } from './site/search';\n\nmountSearch(formatDate);\n\n// src/admin.js\nimport { formatDate } from './shared/date-format';\nimport { mountReport } from './admin/report';\n\nmountReport(formatDate);\n\n// src/shared/date-format.js\nexport function formatDate(date) {\n return date.getFullYear() + '-'\n + String(date.getMonth() + 1).padStart(2, '0');\n}\nЗдесь site/search нужен только сайту, а admin/report — только панели. shared/date-format достижим из обоих стартов. Webpack видит две цепочки, а не один «общий файл». До правила разделения общий модуль может попасть в каждый initial chunk.
Пример учебный. Он показывает направление рёбер и не сообщает размер bundle, время сборки или результат конкретного production-проекта. Размеры нужно измерять в своей версии Webpack и в одинаковом режиме.
\nУ массива другой контракт. Запись entry: ['./src/polyfills.js', './src/site.js'] создаёт один multi-main entry. Webpack загружает файлы в указанном порядке и включает их зависимости в один стартовый граф. Это подходит для полифиллов или подготовительного кода, который всегда нужен сайту.
module.exports = {\n // Один entry и один стартовый граф.\n entry: ['./src/polyfills.js', './src/site.js'],\n\n // Два entry и два независимых старта.\n // Это имеет смысл при двух HTML-документах.\n // entry: {\n // site: './src/site.js',\n // admin: './src/admin.js',\n // },\n};\nТретий вариант vendor: ['jquery'] не делает библиотеку страницей. В Webpack 4 отдельный vendor entry — наследие старой схемы с CommonsChunkPlugin. Для общего кода используйте splitChunks, если это соответствует размеру и загрузке проекта. Не создавайте фиктивную точку запуска только для того, чтобы получить имя файла.
В Webpack 4 разделение задаёт optimization.splitChunks. Правило может искать модули, которые используются в нескольких chunks, и собирать их в отдельный chunk. Оно не обязано выносить каждый общий импорт: на решение влияют тип chunk, минимальный размер, лимиты запросов и cache group.
module.exports = {\n entry: {\n site: './src/site.js',\n admin: './src/admin.js',\n },\n optimization: {\n splitChunks: {\n chunks: 'all',\n cacheGroups: {\n common: {\n name: 'common',\n minChunks: 2,\n minSize: 0,\n chunks: 'all',\n },\n },\n },\n },\n};\nminSize: 0 стоит в примере только для видимости механизма. В рабочем проекте нулевой порог может создать слишком много маленьких запросов. Сначала найдите повторяющийся модуль и его размер. Потом выберите порог, который оправдан кешированием и числом запросов. Не принимайте появление файла common.js за доказательство ускорения.
После разделения в каждом entry всё ещё может быть служебный код Webpack. Runtime хранит сведения о модулях и загрузке chunks. Это не то же самое, что date-format.js или библиотека из node_modules. Для нескольких страниц можно отдельно рассмотреть runtimeChunk: 'single', но это решение меняет служебный слой, а не прикладную зависимость.
Если одна HTML-страница включает два runtime, импортированные модули могут инициализироваться в разных контекстах. Два script-тега не являются нейтральным способом «подключить ещё один модуль». Для одной страницы оставьте один настоящий старт, а дополнительное поведение импортируйте из него. Если код не нужен при первом открытии, рассмотрите динамический import().
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Один модуль виден в двух initial chunks | Оба entry достигают его, общего правила нет или оно не сработало | Посмотреть modules, chunks и entrypoints в stats | Проверить splitChunks, размер и cache group |
В dist появился admin.js | Добавился отдельный entry, а не обязательно лишняя загрузка | Сверить script-теги HTML страницы сайта | Убрать чужой entry из шаблона |
| Массив entry приняли за две страницы | Multi-main entry ошибочно смешали с object syntax | Проверить число HTML-документов и ключей entry | Оставить массив для одного старта или разделить страницы объектом |
| После splitChunks выросло число файлов | Порог слишком низкий или группа дробит мелкие модули | Сравнить размер chunks, количество запросов и кеширование | Поднять порог или сузить cache group |
| Bundle большой, но страница его не запрашивает | Ассет существует в сборке, но не входит в этот entrypoint | Открыть Network и исходный HTML конкретного URL | Не оптимизировать неиспользуемый страницей ассет |
Ниже — пример проверки, а не production-результат. Снимки нужно получить одной версией локального webpack-cli, в одном режиме и на одном наборе исходников. Сравнение development и production скрывает причину за минификацией, source map и разными плагинами.
./node_modules/.bin/webpack --mode production --profile --json > stats-after.json\n\nnode -e "const s=require('./stats-after.json');\nfor (const m of s.modules || []) {\n if ((m.chunks || []).length > 1) {\n console.log(m.size, m.name, m.chunks.join(','));\n }\n}"\nСписок модулей в нескольких chunks — повод для проверки, а не готовый диагноз. Один модуль может легитимно участвовать в начальном и асинхронном пути. Сопоставьте его с entrypoint. Затем откройте HTML и Network для каждой страницы. В Network видны реальные запросы, а stats описывает компиляцию.
\nentry. Убедитесь, что массив означает подготовку одного старта, а объект — независимые страницы.splitChunks. Не создавайте vendor entry для библиотеки.import().Не каждый общий модуль нужно выносить. Маленький модуль может добавить отдельный запрос и не дать выигрыша. Большая библиотека, нужная только модальному окну, не должна попадать в стартовый общий chunk. Для неё лучше проверить отложенную загрузку.
\nЭта статья описывает модель Webpack 4. Современная документация содержит дополнительные поля entry, например dependOn и runtime. Их нельзя механически переносить в конфигурацию Webpack 4. Сначала определите версию сборщика и сверяйтесь с документацией этой версии.
Stats показывает компиляцию, но не доказывает скорость сети. Размер asset может отличаться от переданных байтов после сжатия и кеша. Network показывает один URL и один момент. Для вывода о performance нужны одинаковые условия измерения и отдельный критерий.
\nСборка готова, когда другой инженер без устного пояснения может назвать HTML-документ, его entry, общий chunk и причины его появления. В stats видны ожидаемые entrypoints. В HTML страницы нет чужого entry. В Network запрашиваются только runtime, общие chunks и код этой страницы. Для каждого вынесенного модуля есть объяснение размера и причины загрузки.
\nЕсли один из этих ответов неизвестен, работу нельзя считать законченной. Сначала восстановите связь «HTML → entry → graph → chunk → запрос». Только после этого меняйте пороги, runtime или структуру импортов.
\n