Files

8 lines
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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-сборка стала отдавать два больших стартовых файла. Он открыл stats и заметил код общего модуля <code>date-format</code> в обоих entry. В браузере каталога заодно загрузился скрипт админки. Цена ошибки — лишние байты в критическом пути, второй runtime и код, который браузер скачивает, но не выполняет. Первая проверка должна ответить на вопрос: один HTML действительно запускает два entry или это ошибка шаблона?</p>\n<p>Webpack строит граф от каждого entry. Один и тот же модуль попадает в два начальных bundle, когда оба графа до него доходят. Место файла в репозитории не определяет границу bundle. Сначала нужно установить реальные точки запуска. Затем — решить, какие общие части выделить через <code>optimization.splitChunks</code>, а какие оставить в странице или загрузить позже.</p>\n<h2>Сценарий: два entry в одном релизе</h2><p>Сначала разработчик сверяет фактические <code>&lt;script&gt;</code>-теги в HTML каталога и панели. Затем он сопоставляет их с ключами <code>entry</code> и смотрит, какие модули входят в каждый граф. После этого становится видно, является ли повторение общего кода ожидаемым результатом двух страниц или одна страница ошибочно загружает чужой entry. Только после такой проверки имеет смысл менять конфигурацию.</p><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 &gt; stats-after.json\n\nnode -e &quot;const s=require('./stats-after.json');\nfor (const m of s.modules || []) {\n if ((m.chunks || []).length &gt; 1) {\n console.log(m.size, m.name, m.chunks.join(','));\n }\n}&quot;</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></ul>"
}