{"index":25,"slug":"editorial-2027-04-field-build-evolution","title":"Рост frontend bundle: как найти конкретный input и не чинить симптом","excerpt":"JavaScript-артефакт стал больше, но размер файла не говорит о причине. Разбираем metafile, связываем delta с input и проверяем, что изменилось в output и доставке.","contentHtml":"
После изменения сборки JavaScript-файл стал больше. В отчёте видна общая delta, но не видно, какой импорт её создал. Команда удаляет самую крупную библиотеку по названию. Так легко сломать функцию и не убрать причину: размер мог вырасти из-за нового entry point, дубликата зависимости, отключённого tree-shaking или source map, попавшей в артефакт. Цена ошибки — регресс поведения, лишний сетевой трафик и несколько итераций вслепую.
Тезис статьи простой: рост bundle нужно свести к изменению между двумя наборами входов. Сначала сравнивают metafile или другой отчёт состава сборки. Потом находят input с ненулевой delta, проверяют его связь с chunk и только затем меняют импорт, конфигурацию или delivery. Общий размер файла остаётся симптомом, а не диагнозом.
Сборщик читает entry point и рекурсивно разрешает импорты. Он преобразует модули, удаляет недостижимый код, объединяет часть графа и записывает output. Metafile описывает этот путь в структурированном виде. У esbuild в нём есть разделы inputs и outputs; у конкретного инструмента формат может отличаться, но граница анализа остаётся той же.
Input — файл или модуль, который участвовал в сборке. Его bytes показывают вклад исходного входа в анализ. Output — созданный артефакт. Его размер зависит от преобразования, минификации, разделения chunks и повторного использования общего кода. Передача по сети зависит ещё от gzip или Brotli, заголовков и кэша браузера. Поэтому число в metafile нельзя называть размером загрузки без отдельной проверки.
Source map решает другую задачу. Она связывает преобразованный код с исходными файлами для отладки. Карта может быть большой. Её наличие в каталоге сборки не означает, что её нужно отдавать каждому пользователю. Проверяйте output, HTTP-заголовок и политику публикации отдельно.
Ниже — учебная функция для минимального esbuild-подобного JSON. Она объединяет имена входов из двух отчётов, подставляет ноль для отсутствующего input, считает разницу и сортирует рост сверху. Пример показывает способ поиска. Он не запускает сборку и не доказывает результат в конкретном проекте.
import { summarizeBundleDiff } from './upgrade-2027-04.mjs';\n\nconst before = {\n inputs: {\n 'src/app.ts': { bytes: 4200 },\n 'src/search.ts': { bytes: 1800 },\n 'node_modules/date-fns/index.js': { bytes: 900 }\n }\n};\n\nconst after = {\n inputs: {\n 'src/app.ts': { bytes: 4200 },\n 'src/search.ts': { bytes: 1800 },\n 'node_modules/date-fns/index.js': { bytes: 900 },\n 'node_modules/chart-lib/index.js': { bytes: 7600 }\n }\n};\n\nconsole.log(summarizeBundleDiff(before, after));\n// [{ name: 'node_modules/chart-lib/index.js', before: 0,\n// after: 7600, delta: 7600 }]В реальном отчёте сохраняйте рядом commit, lockfile, команду сборки, режим, entry points, версию runtime и имена output. Иначе два JSON могут выглядеть сравнимыми, хотя один собран с другой конфигурацией. Перед diff проверьте, что пути нормализованы: абсолютный путь рабочей машины и относительный путь CI создадут две разные строки для одного файла.
Следующий вопрос — не «какой input самый большой?», а «как этот input попал в конкретный output?». Ищите связи в разделе outputs или в анализаторе вашего bundler. Если модуль вошёл только в ленивый chunk, изменение не равно росту initial загрузки. Если он попал в общий chunk, его стоимость может распространяться на несколько страниц. Если output не изменился, ищите причину в сжатии, заголовках, кэше или измерении браузера.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Появился новый большой input | Новый импорт или entry point | Найти первый импорт и output, в который он попал | Разделить загрузку, удалить импорт или оставить стоимость с объяснением |
| Старый input вырос | Изменился export, plugin или transform | Сравнить delta input и настройки tree-shaking | Проверить side effects, export и конфигурацию плагина |
| Одна зависимость видна несколькими путями | Дубликаты версий или разные resolver conditions | Сопоставить реальные пути и lockfile | Свести версии, alias или условия разрешения |
| Metafile почти тот же, но ответ тяжелее | Изменились minify, compression или headers | Сравнить raw, gzip/Brotli и HTTP response | Исправить delivery и повторить browser check |
| Выросла source map | Debug artifact публикуется рядом с production output | Проверить каталог, header SourceMap и сетевой запрос | Ограничить публикацию карты нужной среде |
| В metafile нет объяснения | Сравниваются разные входы или другой формат отчёта | Сверить commit, command, target и схему JSON | Пересобрать baseline и candidate в одинаковых условиях |
Представим страницу поиска. В baseline она импортирует форму и таблицу при первом открытии. В candidate в общий модуль добавили визуализацию: import Chart from 'chart-lib'. В metafile появился новый input. Но решение зависит от маршрута импорта.
// Динамическая граница загрузки. Учебный пример.\nconst openChart = async () => {\n const { renderChart } = await import('./chart/render-chart.js');\n return renderChart();\n};\n\nbutton.addEventListener('click', openChart);Если сборщик поддерживает code splitting и конфигурация сохраняет эту границу, библиотека может уйти в отдельный chunk. Тогда initial bundle не обязан вырасти на весь вклад библиотеки. Цена появляется при открытии графика: пользователь ждёт дополнительный запрос и выполнение кода. Нужно измерять оба пути.
Статический импорт даёт другой результат: import { renderChart } from './chart/render-chart.js'. Если модуль достижим из entry point и не исключён настройками, он может попасть в initial output. Это не ошибка само по себе. Для критического пути важнее время до функции, чем минимальный размер каждого файла. Сначала сформулируйте границу загрузки, затем проверьте, сохранил ли её bundler.
Динамический импорт также не гарантирует маленький chunk. Внутри него могут оказаться общие зависимости, полифиллы или набор файлов, созданный шаблонным путём. Для runtime-пути проверяйте сетевой waterfall, размер после сжатия, cache headers и время выполнения. Нельзя делать вывод только по строке delta в отчёте.
Tree-shaking удаляет код, который инструмент считает недостижимым. Побочные эффекты, формат модуля и настройки package могут изменить этот вывод. Если большой input присутствует в отчёте, это ещё не доказывает, что весь исходный файл попал в переданный bundle. Смотрите связь input с output и фактические bytes output.
Дубликат зависимости часто выглядит как два похожих пути: одна копия разрешилась из корня, другая — из вложенного package. Сначала проверьте lockfile и resolver. Alias может уменьшить размер, но сломать пакет, который рассчитывает на другую версию или экспорт. Правило «свести всё к одной версии» применяйте только после проверки совместимости.
Source map нельзя считать частью пользовательского JavaScript без проверки HTTP. Если карта доступна по ссылке из production-ответа, браузер и инструменты разработчика смогут запросить её. Это удобно для отладки, но карта может раскрывать исходники и увеличивать доступный объём артефактов. Решение зависит от политики проекта и среды.
inputs, затем отсортировать изменения по абсолютной delta.Metafile показывает модель сборщика, а не полную стоимость для пользователя. Разные bundler описывают input и output по-разному. Сжатие, HTTP-кэш, CDN, service worker и скорость CPU находятся за пределами одного JSON. Source map может быть создана, но не отдана клиенту. Поэтому сравнение состава нельзя выдавать за измерение производительности страницы.
Нельзя считать исправлением постоянное отключение source map, удаление зависимости по имени или включение агрессивного split без проверки поведения. Нельзя сравнивать отчёты после разных изменений в lockfile и конфигурации. Если входы различаются, сначала восстановите сопоставимые условия; иначе отрицательный результат анализа честнее случайного вывода.
Готовность подтверждается четырьмя артефактами: отчёты baseline и candidate с условиями запуска, diff с конкретным input и output, проверка изменённого пользовательского пути и повторная сборка после действия. Другой инженер должен увидеть, что изменилось, воспроизвести проверку и понять, почему выбранное действие относится к причине. Если причина не найдена, готовым результатом считается зафиксированная граница: состав bundle стабилен, следующий тест идёт на уровне compression, HTTP или браузера.