From 3d44cd4011ee47aea210779f80d4e73bd8bfca54 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 00:53:22 +0300 Subject: [PATCH] Editorial: polish Webpack 4 entry and splitChunks article --- editorial/agent-rewrites/345.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editorial/agent-rewrites/345.json b/editorial/agent-rewrites/345.json index aabacbd..46953b4 100644 --- a/editorial/agent-rewrites/345.json +++ b/editorial/agent-rewrites/345.json @@ -3,5 +3,5 @@ "slug": "editorial-2018-06-practice-webpack-entry", "title": "Webpack 4: две страницы, общие chunks и проверяемая сборка", "excerpt": "Две HTML-страницы могут тянуть один и тот же код дважды. Разбираем границу entry, настройку splitChunks и проверку итоговых ассетов в Webpack 4.", - "contentHtml": "

После добавления страницы заказа production-сборка стала тяжелее. В каталоге появились catalog.js и checkout.js, а jQuery и модуль форматирования цены повторяются в обоих файлах. Пользователь каталога загружает код заказа, хотя не открывает заказ. Ошибка увеличивает первый запрос, усложняет кеширование и маскирует реальную границу страниц.

\n

Причина проста: два entry запускают два графа зависимостей. Webpack видит общий импорт из каждого графа, но не обязан сам превратить его в отдельный файл. Нужны два решения: entry должны описывать реальные точки запуска, а optimization.splitChunks — выделять код, который достигается из нескольких chunks. Общий модуль не становится общим только из-за имени папки shared.

\n

Что именно считает Webpack

\n

Entry — это начало выполнения конкретного сценария. В многостраничном сайте у документа каталога есть свой entry, у документа заказа — свой. Каждый entry строит граф импортов. Если оба графа доходят до src/shared/money.js, модуль присутствует в обоих графах. Дальше оптимизатор сравнивает размер, число использований и правила cache group.

\n

Эти уровни нельзя смешивать. Entry отвечает на вопрос «какой сценарий стартует». splitChunks отвечает на вопрос «какие модули вынести в отдельный chunk». runtimeChunk отвечает за служебный runtime Webpack, который связывает модули и chunks. Третий entry с именем common не заменяет оптимизацию: он сам становится ещё одной точкой запуска.

\n
\"Две
Схема показывает роли файлов. catalog и checkout запускают свои сценарии, а общие chunks подключаются к обеим страницам.
\n

Минимальный граф зависимостей

\n

Ниже учебный пример. Он показывает механизм, но не обещает конкретные размеры файлов и не заменяет сборку проекта. Оба сценария используют jQuery и одну функцию. Код страницы остаётся раздельным.

\n
// src/catalog.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-price]').each(function () {\n  this.textContent = formatPrice(this.dataset.price);\n});\n\n// src/checkout.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-total]').text(formatPrice(window.checkoutTotal));\n\n// src/shared/money.js\nexport function formatPrice(value) {\n  return Number(value).toFixed(2) + ' ₽';\n}
\n

Если собрать такой граф только с двумя entry, сборщик создаст стартовые chunks для каталога и заказа. Общий код может остаться внутри них. Это допустимый результат с точки зрения корректности: оба сценария получают нужный модуль. Но он может быть невыгоден для первой загрузки. Поэтому решение нужно принимать по списку ассетов и сетевому запросу, а не по названию исходной директории.

\n

Конфигурация Webpack 4

\n

В Webpack 4 оставьте в entry только реальные точки запуска. Для общих библиотек используйте splitChunks. В учебном примере minSize: 0 помогает увидеть даже маленький модуль. В рабочей сборке это условие может создать отдельный запрос ради нескольких строк. Порог нужно проверить на размере и времени загрузки конкретного проекта.

\n
// webpack.config.js\nconst path = require('path');\n\nmodule.exports = {\n  mode: 'production',\n  entry: {\n    catalog: './src/catalog.js',\n    checkout: './src/checkout.js',\n  },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[contenthash].js',\n  },\n  optimization: {\n    runtimeChunk: 'single',\n    splitChunks: {\n      chunks: 'all',\n      cacheGroups: {\n        vendors: {\n          test: /[\\/]node_modules[\\/]/,\n          name: 'vendors',\n          chunks: 'all',\n          priority: -10,\n        },\n        common: {\n          name: 'common',\n          minChunks: 2,\n          minSize: 0,\n          chunks: 'all',\n          priority: -20,\n          reuseExistingChunk: true,\n        },\n      },\n    },\n  },\n};
\n

Группа vendors отбирает модули из node_modules. Группа common ищет код, который достигается как минимум из двух chunks. Приоритеты разрешают пересечение правил. reuseExistingChunk: true позволяет повторно использовать уже созданный chunk, когда это возможно.

\n

runtimeChunk: 'single' выносит runtime в отдельный общий файл. Он не выносит jQuery и не делает общий chunk из money.js. За это отвечает splitChunks. Если убрать runtime из проверки, можно ошибочно решить, что два похожих служебных фрагмента — это дублирование прикладного кода.

\n

Как страница подключает результат

\n

Имена с [contenthash] меняются после содержательных изменений. HTML не должен навсегда содержать строку из примера. Шаблонизатор, плагин или серверная сборка должны получить актуальную карту ассетов. Важно также сохранить порядок: runtime, общие chunks, затем код конкретной страницы.

\n
<!-- catalog.html: имена условные, порядок показан явно -->\n<script src=\"/assets/runtime.8ab1.js\"></script>\n<script src=\"/assets/vendors.34cd.js\"></script>\n<script src=\"/assets/common.91ef.js\"></script>\n<script src=\"/assets/catalog.a2b3.js\"></script>
\n

Страница каталога не должна подключать checkout. Страница заказа не должна подключать catalog. Если общий chunk не появился, это не всегда ошибка: модуль мог быть слишком мал, использоваться только одним entry или не пройти условия cache group. Проверяйте фактическую карту сборки.

\n

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

\n
СимптомПричинаПроверкаДействие
Оба entry заметно повторяют jQueryОбщий модуль не попал в подходящую cache groupПосмотреть stats и список модулей в chunksПроверить chunks, minChunks, minSize и путь модуля
На странице заказа запускается каталогHTML подключает соседний entryОткрыть вкладку Network и список script-теговОставить в шаблоне только runtime, нужные общие chunks и checkout
После сборки не найден общий файлКод мал или нужен только одному entryПроверить число достижений модуля в statsНе создавать chunk ради имени; сравнить стоимость запроса и дублирования
В консоли ошибка при стартеНеверный порядок или устаревший hash в HTMLОчистить кеш, проверить ответы Network и порядок загрузкиФормировать HTML из актуальной карты ассетов
Общий chunk стал слишком большимВ него попал код, который нужен только редкому сценариюСопоставить состав chunk с первым экраномОслабить cache group или перенести поздний код в динамический import()
\n

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

\n
  1. Перечислите HTML-документы и подтвердите, что каждый документ запускает отдельный сценарий.
  2. Создайте по одному entry на документ. Не добавляйте в entry библиотеки только ради их общего имени.
  3. Найдите повторяющийся импорт и соберите минимальный учебный граф с одним общим модулем.
  4. Настройте splitChunks и отдельно решите, нужен ли единый runtimeChunk.
  5. Сделайте production-сборку и сохраните stats или карту ассетов до изменения и после него.
  6. Проверьте состав каждого HTML-документа. Сначала runtime и общие chunks, затем собственный entry.
  7. Откройте каждую страницу с очищенным кешем. Проверьте консоль, Network и отсутствие кода соседнего сценария.
  8. Повторите сборку после изменения одного общего модуля и убедитесь, что изменились только ожидаемые hash и chunks.
\n

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

\n

Не всякий общий импорт нужно выносить. Маленький модуль может дешевле повторить, чем загружать новый файл. Большая библиотека может использоваться двумя entry, но быть нужна только после открытия модального окна. В таком случае общий стартовый chunk ухудшит первую загрузку. Для позднего кода рассмотрите динамический import() и проверяйте его отдельным сценарием.

\n

Эта схема относится к Webpack 4. В ней нет современного API dependOn, и нельзя переносить настройки из другой версии без сверки документации. Сборка также не исправит ошибку, если один HTML намеренно подключает два entry: нужно отдельно проверить количество runtime и порядок запуска.

\n

Учебный пример не доказывает выигрыш в production. На него влияют размер модулей, HTTP-протокол, кеш, HTML, который формирует сервер, и порядок загрузки. Сравнивайте измеренные ассеты и запросы своего проекта.

\n

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

\n

Считайте работу готовой, когда для каждой страницы можно показать четыре доказательства: entry, который запускает сценарий; список общих chunks; отсутствие entry соседней страницы; успешный запуск с очищенным кешем. Эти сведения должны совпадать в конфигурации, сгенерированном HTML, stats и вкладке Network. Размер одного стартового файла сам по себе не является критерием.

\n

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

" + "contentHtml": "

Рассмотрим учебный сценарий после добавления страницы заказа. В сборке появились catalog.js и checkout.js, а jQuery и модуль форматирования цены попали в оба entry-бандла. При этом сама страница каталога не обязана загружать код заказа: общий модуль и соседний entry — разные проблемы. Если их смешать, первый запрос растёт, кеширование становится менее предсказуемым, а граница страниц теряется.

\n

Первое предположение — ошибка в импорте: кажется, что каталог случайно подтянул checkout.js. Проверка сгенерированного HTML показывает, что лишнего entry там нет. В stats повторяются именно общие зависимости. Значит, нужно разделить два вопроса: entry описывает точку запуска документа, а optimization.splitChunks может вынести модуль, который используется несколькими chunks. Общий модуль не становится общим только из-за имени папки shared.

\n

Что именно считает Webpack

\n

Entry — это точка, с которой Webpack начинает строить граф зависимостей. В многостраничном сайте у документа каталога есть свой entry, у документа заказа — свой. Два entry дают два графа импортов. Если оба графа доходят до src/shared/money.js, этот модуль является общей зависимостью, но способ его выдачи зависит от настроек оптимизации.

\n

Entry отвечает на вопрос «какой сценарий запускается». splitChunks отвечает на вопрос «какие модули можно вынести в отдельный chunk». runtimeChunk отвечает за служебный runtime Webpack, который связывает модули и chunks. Третий entry с именем common не заменяет оптимизацию: он сам становится ещё одной точкой запуска и может изменить поведение страницы.

\n
Две страницы Webpack 4 запускают разные entry и используют общие runtime, vendors и common chunks
У каталога и заказа свои entry. Общие chunks подключаются к нужному документу, но соседний page-entry в его HTML не появляется.
\n

Минимальный граф зависимостей

\n

Ниже учебный пример. Он показывает механизм, но не обещает конкретные размеры файлов и не заменяет сборку проекта. Оба сценария статически используют jQuery и одну функцию. Код страницы остаётся раздельным.

\n
// src/catalog.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-price]').each(function () {\n  this.textContent = formatPrice(this.dataset.price);\n});\n\n// src/checkout.js\nimport $ from 'jquery';\nimport { formatPrice } from './shared/money';\n\n$('[data-total]').text(formatPrice(window.checkoutTotal));\n\n// src/shared/money.js\nexport function formatPrice(value) {\n  return Number(value).toFixed(2) + ' ₽';\n}
\n

Если собрать этот граф без подходящего правила splitChunks, Webpack может оставить повторяющуюся зависимость в стартовых chunks. Это не означает, что каталог получил весь checkout.js: оба entry просто содержат код, который им нужен. Для проверки сохраните stats.json и найдите один и тот же модуль в обоих entry. Решение принимайте по фактическим ассетам и сетевым запросам, а не по названию исходной директории.

\n

Конфигурация Webpack 4

\n

В Webpack 4 оставьте в entry реальные точки запуска документов. Для общих библиотек используйте splitChunks. В учебном примере minSize: 0 помогает увидеть даже маленький модуль. В рабочей сборке это условие может создать отдельный запрос ради нескольких строк, поэтому порог проверяют на размере и времени загрузки конкретного проекта.

\n
// webpack.config.js\nconst path = require('path');\n\nmodule.exports = {\n  mode: 'production',\n  entry: {\n    catalog: './src/catalog.js',\n    checkout: './src/checkout.js',\n  },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].[contenthash].js',\n  },\n  optimization: {\n    runtimeChunk: 'single',\n    splitChunks: {\n      chunks: 'all',\n      cacheGroups: {\n        vendors: {\n          test: /[\\/]node_modules[\\/]/,\n          name: 'vendors',\n          chunks: 'all',\n          priority: -10,\n        },\n        common: {\n          name: 'common',\n          minChunks: 2,\n          minSize: 0,\n          chunks: 'all',\n          priority: -20,\n          reuseExistingChunk: true,\n        },\n      },\n    },\n  },\n};
\n

Группа vendors отбирает модули из node_modules, поэтому в примере в неё попадёт jQuery. Группа common ищет код, который достигается как минимум из двух chunks. Приоритет -10 выше, чем -20, поэтому пересекающаяся зависимость сначала подходит под более приоритетную группу. Конкретный состав результата всё равно нужно проверить в stats.

\n

runtimeChunk: 'single' создаёт один runtime-файл для всех сгенерированных chunks. В многостраничном сайте это помогает кешировать runtime, но каждый HTML-документ должен подключить этот файл не более одного раза. Если два entry намеренно запускаются на одной странице, нужно отдельно проверить их совместимость и число runtime: один runtime не исправляет ошибку в разметке.

\n

Как страница подключает результат

\n

Имена с [contenthash] меняются после содержательных изменений. HTML не должен навсегда содержать строки из примера: шаблонизатор или серверная сборка должны получить актуальную карту ассетов. Для обычного статического подключения ожидаемый порядок такой: runtime, общие chunks, затем код конкретной страницы. Фактический порядок проверяйте по карте ассетов и сгенерированному HTML.

\n
<!-- catalog.html: имена условные, порядок показан явно -->\n<script src='/assets/runtime.8ab1.js'></script>\n<script src='/assets/vendors.34cd.js'></script>\n<script src='/assets/common.91ef.js'></script>\n<script src='/assets/catalog.a2b3.js'></script>
\n

Страница каталога не должна подключать checkout, а страница заказа — catalog. Если общий chunk не появился, это не всегда ошибка: модуль мог быть слишком мал, использоваться только одним entry или не пройти условия cache group. Откройте HTML и Network, чтобы отличить лишний page-entry от допустимого дублирования или результата других оптимизаций.

\n

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

\n
СимптомПричинаПроверкаДействие
В stats один модуль есть в обоих entryОбщая зависимость не попала в подходящую cache groupСопоставить модуль, chunk и entryПроверить chunks, minChunks, minSize и путь модуля
На странице заказа запускается каталогHTML подключает соседний page-entryОткрыть список script-тегов и вкладку NetworkОставить runtime, нужные общие chunks и checkout
После сборки не найден общий файлКод мал или нужен только одному entryПроверить число достижений модуля в statsНе создавать chunk ради имени; сравнить стоимость запроса и дублирования
В консоли ошибка при стартеНеверный порядок или устаревший hash в HTMLОчистить кеш, проверить ответы Network и карту ассетовФормировать HTML из актуальных имён и проверить порядок загрузки
Общий chunk стал слишком большимВ него попал код редкого сценарияСопоставить состав chunk с первым экраномОслабить cache group или перенести поздний код в динамический import()
\n

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

\n
  1. Перечислите HTML-документы и подтвердите, что каждый документ запускает отдельный сценарий.
  2. Создайте по одному entry на документ. Не добавляйте в entry библиотеки только ради их общего имени.
  3. Найдите повторяющийся импорт и сохраните stats до изменения конфигурации.
  4. Настройте splitChunks и отдельно решите, нужен ли единый runtimeChunk.
  5. Сделайте production-сборку и сравните список модулей, chunks и размеры ассетов до и после.
  6. Проверьте HTML каждого документа. В нём должен быть собственный entry и только необходимые общие chunks.
  7. Откройте обе страницы с очищенным кешем. Проверьте консоль, Network и отсутствие кода соседнего сценария.
  8. Измените один общий модуль, повторите сборку и убедитесь, что изменились ожидаемые hash и chunks, а не случайный page-entry.
\n

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

\n

Не всякий общий импорт нужно выносить. Маленький модуль может дешевле повторить, чем загружать новый файл. Большая библиотека может использоваться двумя entry, но быть нужна только после открытия модального окна. Тогда общий стартовый chunk ухудшит первую загрузку; для позднего кода рассмотрите динамический import() и проверьте его отдельным сценарием.

\n

Эта схема относится к Webpack 4. В ней нет API dependOn из Webpack 5, и настройки другой версии нельзя переносить без сверки документации. Webpack также не исправит ошибку, если один HTML намеренно подключает два page-entry: нужно отдельно проверить количество runtime, порядок запуска и побочные эффекты модулей.

\n

Учебный пример не доказывает выигрыш в production. На результат влияют размер модулей, HTTP-протокол, кеш, HTML, который формирует сервер, и порядок загрузки. Сравнивайте измеренные ассеты и запросы своего проекта; если выигрыш не подтверждается, верните конфигурацию к меньшему числу запросов.

\n

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

\n

Считайте работу готовой, когда для каждой страницы можно показать четыре доказательства: entry, который запускает сценарий; список общих chunks; отсутствие entry соседней страницы; успешный запуск с очищенным кешем. Эти сведения должны совпадать в конфигурации, сгенерированном HTML, stats и вкладке Network. Размер одного стартового файла сам по себе не является критерием. Если проверка снова показывает лишний checkout в каталоге, сначала исправьте HTML, а не добавляйте ещё одну cache group.

\n

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

" }