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 и код, который браузер скачивает, но не выполняет. Если исправить только имя файла или перенести модуль в другую папку, причина останется.
Тезис: 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 или структуру импортов.
\nadmin.js, после чего production-сборка стала отдавать два больших стартовых файла. Он открыл stats и заметил код общего модуля date-format в обоих entry. В браузере каталога заодно загрузился скрипт админки. Цена ошибки — лишние байты в критическом пути, второй runtime и код, который браузер скачивает, но не выполняет. Первая проверка должна ответить на вопрос: один HTML действительно запускает два entry или это ошибка шаблона?\nWebpack строит граф от каждого entry. Один и тот же модуль попадает в два начальных bundle, когда оба графа до него доходят. Место файла в репозитории не определяет границу bundle. Сначала нужно установить реальные точки запуска. Затем — решить, какие общие части выделить через optimization.splitChunks, а какие оставить в странице или загрузить позже.
Сначала разработчик сверяет фактические <script>-теги в HTML каталога и панели. Затем он сопоставляет их с ключами entry и смотрит, какие модули входят в каждый граф. После этого становится видно, является ли повторение общего кода ожидаемым результатом двух страниц или одна страница ошибочно загружает чужой entry. Только после такой проверки имеет смысл менять конфигурацию.
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