{"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. Общий размер файла остаётся симптомом, а не диагнозом.

Как проходит сигнал от input к браузеру

Сборщик читает entry point и рекурсивно разрешает импорты. Он преобразует модули, удаляет недостижимый код, объединяет часть графа и записывает output. Metafile описывает этот путь в структурированном виде. У esbuild в нём есть разделы inputs и outputs; у конкретного инструмента формат может отличаться, но граница анализа остаётся той же.

Input — файл или модуль, который участвовал в сборке. Его bytes показывают вклад исходного входа в анализ. Output — созданный артефакт. Его размер зависит от преобразования, минификации, разделения chunks и повторного использования общего кода. Передача по сети зависит ещё от gzip или Brotli, заголовков и кэша браузера. Поэтому число в metafile нельзя называть размером загрузки без отдельной проверки.

Source map решает другую задачу. Она связывает преобразованный код с исходными файлами для отладки. Карта может быть большой. Её наличие в каталоге сборки не означает, что её нужно отдавать каждому пользователю. Проверяйте output, HTTP-заголовок и политику публикации отдельно.

\"Цикл
Общий симптом проходит несколько границ: input, chunk, output и HTTP-ответ. Исправление выбирают после перехода к конкретному слою.

Учебный diff двух отчётов

Ниже — учебная функция для минимального 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 не изменился, ищите причину в сжатии, заголовках, кэше или измерении браузера.

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

Матрица диагностики роста bundle
СимптомПричинаПроверкаДействие
Появился новый большой 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 mapDebug 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-ответа, браузер и инструменты разработчика смогут запросить её. Это удобно для отладки, но карта может раскрывать исходники и увеличивать доступный объём артефактов. Решение зависит от политики проекта и среды.

Действия по порядку

  1. Зафиксировать baseline и candidate: commit, lockfile, runtime, команда, режим, entry points, flags и output directory.
  2. Собрать оба отчёта состава на одинаковом окружении. Записать exit code и не смешивать cold cache с warm cache без пометки.
  3. Нормализовать пути и схему JSON. Запустить diff по inputs, затем отсортировать изменения по абсолютной delta.
  4. Для каждого заметного input найти output и chunk. Отделить initial, lazy и shared части.
  5. Проверить причину: импорт, версия зависимости, resolver, plugin, tree-shaking, minify или source map.
  6. Сделать одно изменение. Пересобрать candidate и повторить diff, чтобы увидеть, исчезла ли именно заявленная delta.
  7. Проверить браузерный путь: network transfer, compression, cache hit, время загрузки lazy chunk и ошибки runtime.
  8. Зафиксировать отрицательный путь: если metafile стабилен, не менять импорт, а перейти к delivery или browser measurement.

Ограничения и критерий готовности

Metafile показывает модель сборщика, а не полную стоимость для пользователя. Разные bundler описывают input и output по-разному. Сжатие, HTTP-кэш, CDN, service worker и скорость CPU находятся за пределами одного JSON. Source map может быть создана, но не отдана клиенту. Поэтому сравнение состава нельзя выдавать за измерение производительности страницы.

Нельзя считать исправлением постоянное отключение source map, удаление зависимости по имени или включение агрессивного split без проверки поведения. Нельзя сравнивать отчёты после разных изменений в lockfile и конфигурации. Если входы различаются, сначала восстановите сопоставимые условия; иначе отрицательный результат анализа честнее случайного вывода.

Готовность подтверждается четырьмя артефактами: отчёты baseline и candidate с условиями запуска, diff с конкретным input и output, проверка изменённого пользовательского пути и повторная сборка после действия. Другой инженер должен увидеть, что изменилось, воспроизвести проверку и понять, почему выбранное действие относится к причине. Если причина не найдена, готовым результатом считается зафиксированная граница: состав bundle стабилен, следующий тест идёт на уровне compression, HTTP или браузера.

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

"}