diff --git a/editorial/agent-rewrites/344.json b/editorial/agent-rewrites/344.json index a4f30fc..d1d24eb 100644 --- a/editorial/agent-rewrites/344.json +++ b/editorial/agent-rewrites/344.json @@ -3,5 +3,5 @@ "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 и код, который браузер скачивает, но не выполняет. Если исправить только имя файла или перенести модуль в другую папку, причина останется.

\n

Тезис: Webpack строит граф от каждого entry. Один и тот же модуль попадает в два начальных bundle, когда оба графа до него доходят. Место файла в репозитории не определяет границу bundle. Сначала нужно установить реальные точки запуска. Затем — решить, какие общие части выделить через optimization.splitChunks, а какие оставить в странице или загрузить позже.

\n

Что именно означает entry

\n

Entry задаёт начало обхода. Webpack читает entry, проходит его import и require, затем повторяет обход для найденных модулей и ассетов. Так появляется внутренний граф зависимостей. Output-файл — только один из результатов этого графа.

\n

Объект с ключами site и admin означает два самостоятельных старта. Обычно это верно для двух HTML-документов: сервер отдаёт каталог с одним сценарием и панель с другим. Если один HTML подключает оба entry, конфигурация описывает не две страницы, а два старта внутри одной страницы. Это отдельная ошибка.

\n
\"Два
Два entry проходят к своим модулям и оба достигают общих зависимостей. Граф объясняет дублирование лучше, чем список файлов в dist.
\n

Минимальный пример

\n
// 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.

\n

Пример учебный. Он показывает направление рёбер и не сообщает размер bundle, время сборки или результат конкретного production-проекта. Размеры нужно измерять в своей версии Webpack и в одинаковом режиме.

\n

Массив entry не создаёт вторую страницу

\n

У массива другой контракт. Запись entry: ['./src/polyfills.js', './src/site.js'] создаёт один multi-main entry. Webpack загружает файлы в указанном порядке и включает их зависимости в один стартовый граф. Это подходит для полифиллов или подготовительного кода, который всегда нужен сайту.

\n
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, если это соответствует размеру и загрузке проекта. Не создавайте фиктивную точку запуска только для того, чтобы получить имя файла.

\n

Как разделить общий участок графа

\n

В Webpack 4 разделение задаёт optimization.splitChunks. Правило может искать модули, которые используются в нескольких chunks, и собирать их в отдельный chunk. Оно не обязано выносить каждый общий импорт: на решение влияют тип chunk, минимальный размер, лимиты запросов и cache group.

\n
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};
\n

minSize: 0 стоит в примере только для видимости механизма. В рабочем проекте нулевой порог может создать слишком много маленьких запросов. Сначала найдите повторяющийся модуль и его размер. Потом выберите порог, который оправдан кешированием и числом запросов. Не принимайте появление файла common.js за доказательство ускорения.

\n

Runtime не равен общему модулю

\n

После разделения в каждом entry всё ещё может быть служебный код Webpack. Runtime хранит сведения о модулях и загрузке chunks. Это не то же самое, что date-format.js или библиотека из node_modules. Для нескольких страниц можно отдельно рассмотреть runtimeChunk: 'single', но это решение меняет служебный слой, а не прикладную зависимость.

\n

Если одна HTML-страница включает два runtime, импортированные модули могут инициализироваться в разных контекстах. Два script-тега не являются нейтральным способом «подключить ещё один модуль». Для одной страницы оставьте один настоящий старт, а дополнительное поведение импортируйте из него. Если код не нужен при первом открытии, рассмотрите динамический import().

\n

Симптом → причина → проверка → действие

\n
Диагностика дублирования и лишней загрузки
СимптомПричинаПроверкаДействие
Один модуль виден в двух 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Не оптимизировать неиспользуемый страницей ассет
\n

Проверка на учебной сборке

\n

Ниже — пример проверки, а не production-результат. Снимки нужно получить одной версией локального webpack-cli, в одном режиме и на одном наборе исходников. Сравнение development и production скрывает причину за минификацией, source map и разными плагинами.

\n
./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 описывает компиляцию.

\n

Порядок действий

\n
  1. Запишите все HTML-документы и по одному ожидаемому сценарию для каждого.
  2. Для каждого документа найдите фактические script-теги и назовите единственный настоящий старт.
  3. Сверьте это с конфигурацией entry. Убедитесь, что массив означает подготовку одного старта, а объект — независимые страницы.
  4. Снимите production stats до изменения и после него одной командой сборки.
  5. Найдите модуль, который повторяется в initial chunks, и проверьте, действительно ли он нужен обеим страницам.
  6. Добавьте минимальное правило splitChunks. Не создавайте vendor entry для библиотеки.
  7. Повторите сборку и проверьте состав entrypoints, размер chunks и число запросов.
  8. Откройте каждый HTML в браузере. Проверьте Network, порядок script-тегов и отсутствие чужого entry.
  9. Если код нужен только после действия пользователя, сравните общий initial chunk с динамическим import().
\n

Отрицательный путь и ограничения

\n

Не каждый общий модуль нужно выносить. Маленький модуль может добавить отдельный запрос и не дать выигрыша. Большая библиотека, нужная только модальному окну, не должна попадать в стартовый общий chunk. Для неё лучше проверить отложенную загрузку.

\n

Эта статья описывает модель Webpack 4. Современная документация содержит дополнительные поля entry, например dependOn и runtime. Их нельзя механически переносить в конфигурацию Webpack 4. Сначала определите версию сборщика и сверяйтесь с документацией этой версии.

\n

Stats показывает компиляцию, но не доказывает скорость сети. Размер asset может отличаться от переданных байтов после сжатия и кеша. Network показывает один URL и один момент. Для вывода о performance нужны одинаковые условия измерения и отдельный критерий.

\n

Критерий готовности

\n

Сборка готова, когда другой инженер без устного пояснения может назвать HTML-документ, его entry, общий chunk и причины его появления. В stats видны ожидаемые entrypoints. В HTML страницы нет чужого entry. В Network запрашиваются только runtime, общие chunks и код этой страницы. Для каждого вынесенного модуля есть объяснение размера и причины загрузки.

\n

Если один из этих ответов неизвестен, работу нельзя считать законченной. Сначала восстановите связь «HTML → entry → graph → chunk → запрос». Только после этого меняйте пороги, runtime или структуру импортов.

\n

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

\n