8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 344,
|
||
"slug": "editorial-2018-06-mechanism-webpack-entry",
|
||
"title": "Webpack 4: почему общий модуль попадает в два entry bundle",
|
||
"excerpt": "После добавления второго entry общий модуль может оказаться в обоих стартовых bundle. Разбираем граф зависимостей, смысл массива entry, splitChunks и проверку результата через stats и HTML.",
|
||
"contentHtml": "<p>После добавления <code>admin.js</code> production-сборка начинает отдавать два больших стартовых файла. В обоих находится <code>date-format.js</code>. Иногда обычная страница ещё и загружает скрипт админки. Цена ошибки — лишние байты в критическом пути, второй runtime и код, который браузер скачивает, но не выполняет. Если исправить только имя файла или перенести модуль в другую папку, причина останется.</p>\n<p><strong>Тезис:</strong> Webpack строит граф от каждого entry. Один и тот же модуль попадает в два начальных bundle, когда оба графа до него доходят. Место файла в репозитории не определяет границу bundle. Сначала нужно установить реальные точки запуска. Затем — решить, какие общие части выделить через <code>optimization.splitChunks</code>, а какие оставить в странице или загрузить позже.</p>\n<h2>Что именно означает entry</h2>\n<p>Entry задаёт начало обхода. Webpack читает entry, проходит его <code>import</code> и <code>require</code>, затем повторяет обход для найденных модулей и ассетов. Так появляется внутренний граф зависимостей. Output-файл — только один из результатов этого графа.</p>\n<p>Объект с ключами <code>site</code> и <code>admin</code> означает два самостоятельных старта. Обычно это верно для двух HTML-документов: сервер отдаёт каталог с одним сценарием и панель с другим. Если один HTML подключает оба entry, конфигурация описывает не две страницы, а два старта внутри одной страницы. Это отдельная ошибка.</p>\n<figure><img src=\"/assets/editorial/2018/webpack-entry-graph-2018.svg\" alt=\"Два entry Webpack 4 сходятся к общему модулю date-format и библиотеке jquery\" loading=\"lazy\" /><figcaption>Два entry проходят к своим модулям и оба достигают общих зависимостей. Граф объясняет дублирование лучше, чем список файлов в dist.</figcaption></figure>\n<h2>Минимальный пример</h2>\n<pre><code>// 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}</code></pre>\n<p>Здесь <code>site/search</code> нужен только сайту, а <code>admin/report</code> — только панели. <code>shared/date-format</code> достижим из обоих стартов. Webpack видит две цепочки, а не один «общий файл». До правила разделения общий модуль может попасть в каждый initial chunk.</p>\n<p>Пример учебный. Он показывает направление рёбер и не сообщает размер bundle, время сборки или результат конкретного production-проекта. Размеры нужно измерять в своей версии Webpack и в одинаковом режиме.</p>\n<h2>Массив entry не создаёт вторую страницу</h2>\n<p>У массива другой контракт. Запись <code>entry: ['./src/polyfills.js', './src/site.js']</code> создаёт один multi-main entry. Webpack загружает файлы в указанном порядке и включает их зависимости в один стартовый граф. Это подходит для полифиллов или подготовительного кода, который всегда нужен сайту.</p>\n<pre><code>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};</code></pre>\n<p>Третий вариант <code>vendor: ['jquery']</code> не делает библиотеку страницей. В Webpack 4 отдельный vendor entry — наследие старой схемы с CommonsChunkPlugin. Для общего кода используйте <code>splitChunks</code>, если это соответствует размеру и загрузке проекта. Не создавайте фиктивную точку запуска только для того, чтобы получить имя файла.</p>\n<h2>Как разделить общий участок графа</h2>\n<p>В Webpack 4 разделение задаёт <code>optimization.splitChunks</code>. Правило может искать модули, которые используются в нескольких chunks, и собирать их в отдельный chunk. Оно не обязано выносить каждый общий импорт: на решение влияют тип chunk, минимальный размер, лимиты запросов и cache group.</p>\n<pre><code>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};</code></pre>\n<p><code>minSize: 0</code> стоит в примере только для видимости механизма. В рабочем проекте нулевой порог может создать слишком много маленьких запросов. Сначала найдите повторяющийся модуль и его размер. Потом выберите порог, который оправдан кешированием и числом запросов. Не принимайте появление файла <code>common.js</code> за доказательство ускорения.</p>\n<h2>Runtime не равен общему модулю</h2>\n<p>После разделения в каждом entry всё ещё может быть служебный код Webpack. Runtime хранит сведения о модулях и загрузке chunks. Это не то же самое, что <code>date-format.js</code> или библиотека из <code>node_modules</code>. Для нескольких страниц можно отдельно рассмотреть <code>runtimeChunk: 'single'</code>, но это решение меняет служебный слой, а не прикладную зависимость.</p>\n<p>Если одна HTML-страница включает два runtime, импортированные модули могут инициализироваться в разных контекстах. Два script-тега не являются нейтральным способом «подключить ещё один модуль». Для одной страницы оставьте один настоящий старт, а дополнительное поведение импортируйте из него. Если код не нужен при первом открытии, рассмотрите динамический <code>import()</code>.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика дублирования и лишней загрузки</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Один модуль виден в двух initial chunks</td><td>Оба entry достигают его, общего правила нет или оно не сработало</td><td>Посмотреть <code>modules</code>, <code>chunks</code> и <code>entrypoints</code> в stats</td><td>Проверить <code>splitChunks</code>, размер и cache group</td></tr><tr><td>В <code>dist</code> появился <code>admin.js</code></td><td>Добавился отдельный entry, а не обязательно лишняя загрузка</td><td>Сверить script-теги HTML страницы сайта</td><td>Убрать чужой entry из шаблона</td></tr><tr><td>Массив entry приняли за две страницы</td><td>Multi-main entry ошибочно смешали с object syntax</td><td>Проверить число HTML-документов и ключей entry</td><td>Оставить массив для одного старта или разделить страницы объектом</td></tr><tr><td>После splitChunks выросло число файлов</td><td>Порог слишком низкий или группа дробит мелкие модули</td><td>Сравнить размер chunks, количество запросов и кеширование</td><td>Поднять порог или сузить cache group</td></tr><tr><td>Bundle большой, но страница его не запрашивает</td><td>Ассет существует в сборке, но не входит в этот entrypoint</td><td>Открыть Network и исходный HTML конкретного URL</td><td>Не оптимизировать неиспользуемый страницей ассет</td></tr></tbody></table>\n<h2>Проверка на учебной сборке</h2>\n<p>Ниже — пример проверки, а не production-результат. Снимки нужно получить одной версией локального <code>webpack-cli</code>, в одном режиме и на одном наборе исходников. Сравнение development и production скрывает причину за минификацией, source map и разными плагинами.</p>\n<pre><code>./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}"</code></pre>\n<p>Список модулей в нескольких chunks — повод для проверки, а не готовый диагноз. Один модуль может легитимно участвовать в начальном и асинхронном пути. Сопоставьте его с entrypoint. Затем откройте HTML и Network для каждой страницы. В Network видны реальные запросы, а stats описывает компиляцию.</p>\n<h2>Порядок действий</h2>\n<ol><li>Запишите все HTML-документы и по одному ожидаемому сценарию для каждого.</li><li>Для каждого документа найдите фактические script-теги и назовите единственный настоящий старт.</li><li>Сверьте это с конфигурацией <code>entry</code>. Убедитесь, что массив означает подготовку одного старта, а объект — независимые страницы.</li><li>Снимите production stats до изменения и после него одной командой сборки.</li><li>Найдите модуль, который повторяется в initial chunks, и проверьте, действительно ли он нужен обеим страницам.</li><li>Добавьте минимальное правило <code>splitChunks</code>. Не создавайте vendor entry для библиотеки.</li><li>Повторите сборку и проверьте состав entrypoints, размер chunks и число запросов.</li><li>Откройте каждый HTML в браузере. Проверьте Network, порядок script-тегов и отсутствие чужого entry.</li><li>Если код нужен только после действия пользователя, сравните общий initial chunk с динамическим <code>import()</code>.</li></ol>\n<h2>Отрицательный путь и ограничения</h2>\n<p>Не каждый общий модуль нужно выносить. Маленький модуль может добавить отдельный запрос и не дать выигрыша. Большая библиотека, нужная только модальному окну, не должна попадать в стартовый общий chunk. Для неё лучше проверить отложенную загрузку.</p>\n<p>Эта статья описывает модель Webpack 4. Современная документация содержит дополнительные поля entry, например <code>dependOn</code> и <code>runtime</code>. Их нельзя механически переносить в конфигурацию Webpack 4. Сначала определите версию сборщика и сверяйтесь с документацией этой версии.</p>\n<p>Stats показывает компиляцию, но не доказывает скорость сети. Размер asset может отличаться от переданных байтов после сжатия и кеша. Network показывает один URL и один момент. Для вывода о performance нужны одинаковые условия измерения и отдельный критерий.</p>\n<h2>Критерий готовности</h2>\n<p>Сборка готова, когда другой инженер без устного пояснения может назвать HTML-документ, его entry, общий chunk и причины его появления. В stats видны ожидаемые entrypoints. В HTML страницы нет чужого entry. В Network запрашиваются только runtime, общие chunks и код этой страницы. Для каждого вынесенного модуля есть объяснение размера и причины загрузки.</p>\n<p>Если один из этих ответов неизвестен, работу нельзя считать законченной. Сначала восстановите связь «HTML → entry → graph → chunk → запрос». Только после этого меняйте пороги, runtime или структуру импортов.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://webpack.js.org/concepts/entry-points/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack: Entry Points</a> — синтаксис entry, multi-main entry и отдельные графы многостраничного приложения.</li><li><a href=\"https://webpack.js.org/plugins/split-chunks-plugin/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack: SplitChunksPlugin</a> — правила выделения общих chunks и ограничения оптимизации.</li><li><a href=\"https://webpack.js.org/configuration/stats/\" target=\"_blank\" rel=\"noopener noreferrer\">Webpack: Stats configuration</a> — формат и поля статистики сборки для проверки ассетов, chunks и модулей.</li></u"
|
||
}
|