2 lines
17 KiB
JSON
2 lines
17 KiB
JSON
{"index":25,"slug":"editorial-2027-04-field-build-evolution","title":"Рост frontend bundle: как найти конкретный input и не чинить симптом","excerpt":"JavaScript-артефакт стал больше, но размер файла не говорит о причине. Разбираем metafile, связываем delta с input и проверяем, что изменилось в output и доставке.","contentHtml":"<p>После изменения сборки JavaScript-файл стал больше. В отчёте видна общая delta, но не видно, какой импорт её создал. Команда удаляет самую крупную библиотеку по названию. Так легко сломать функцию и не убрать причину: размер мог вырасти из-за нового entry point, дубликата зависимости, отключённого tree-shaking или source map, попавшей в артефакт. Цена ошибки — регресс поведения, лишний сетевой трафик и несколько итераций вслепую.</p><p>Тезис статьи простой: рост bundle нужно свести к изменению между двумя наборами входов. Сначала сравнивают metafile или другой отчёт состава сборки. Потом находят input с ненулевой delta, проверяют его связь с chunk и только затем меняют импорт, конфигурацию или delivery. Общий размер файла остаётся симптомом, а не диагнозом.</p><h2>Как проходит сигнал от input к браузеру</h2><p>Сборщик читает entry point и рекурсивно разрешает импорты. Он преобразует модули, удаляет недостижимый код, объединяет часть графа и записывает output. Metafile описывает этот путь в структурированном виде. У esbuild в нём есть разделы <code>inputs</code> и <code>outputs</code>; у конкретного инструмента формат может отличаться, но граница анализа остаётся той же.</p><p>Input — файл или модуль, который участвовал в сборке. Его <code>bytes</code> показывают вклад исходного входа в анализ. Output — созданный артефакт. Его размер зависит от преобразования, минификации, разделения chunks и повторного использования общего кода. Передача по сети зависит ещё от gzip или Brotli, заголовков и кэша браузера. Поэтому число в metafile нельзя называть размером загрузки без отдельной проверки.</p><p>Source map решает другую задачу. Она связывает преобразованный код с исходными файлами для отладки. Карта может быть большой. Её наличие в каталоге сборки не означает, что её нужно отдавать каждому пользователю. Проверяйте output, HTTP-заголовок и политику публикации отдельно.</p><figure><img src=\"/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg\" alt=\"Цикл разбора роста bundle: сравнение metafile, поиск input с delta, проверка chunk и source map, затем повторная сборка\" loading=\"lazy\" /><figcaption>Общий симптом проходит несколько границ: input, chunk, output и HTTP-ответ. Исправление выбирают после перехода к конкретному слою.</figcaption></figure><h2>Учебный diff двух отчётов</h2><p>Ниже — учебная функция для минимального esbuild-подобного JSON. Она объединяет имена входов из двух отчётов, подставляет ноль для отсутствующего input, считает разницу и сортирует рост сверху. Пример показывает способ поиска. Он не запускает сборку и не доказывает результат в конкретном проекте.</p><pre><code>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 }]</code></pre><p>В реальном отчёте сохраняйте рядом commit, lockfile, команду сборки, режим, entry points, версию runtime и имена output. Иначе два JSON могут выглядеть сравнимыми, хотя один собран с другой конфигурацией. Перед diff проверьте, что пути нормализованы: абсолютный путь рабочей машины и относительный путь CI создадут две разные строки для одного файла.</p><p>Следующий вопрос — не «какой input самый большой?», а «как этот input попал в конкретный output?». Ищите связи в разделе outputs или в анализаторе вашего bundler. Если модуль вошёл только в ленивый chunk, изменение не равно росту initial загрузки. Если он попал в общий chunk, его стоимость может распространяться на несколько страниц. Если output не изменился, ищите причину в сжатии, заголовках, кэше или измерении браузера.</p><h2>Симптом → причина → проверка → действие</h2><div class=\"table-scroll\"><table><caption>Матрица диагностики роста bundle</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Появился новый большой input</td><td>Новый импорт или entry point</td><td>Найти первый импорт и output, в который он попал</td><td>Разделить загрузку, удалить импорт или оставить стоимость с объяснением</td></tr><tr><td>Старый input вырос</td><td>Изменился export, plugin или transform</td><td>Сравнить delta input и настройки tree-shaking</td><td>Проверить side effects, export и конфигурацию плагина</td></tr><tr><td>Одна зависимость видна несколькими путями</td><td>Дубликаты версий или разные resolver conditions</td><td>Сопоставить реальные пути и lockfile</td><td>Свести версии, alias или условия разрешения</td></tr><tr><td>Metafile почти тот же, но ответ тяжелее</td><td>Изменились minify, compression или headers</td><td>Сравнить raw, gzip/Brotli и HTTP response</td><td>Исправить delivery и повторить browser check</td></tr><tr><td>Выросла source map</td><td>Debug artifact публикуется рядом с production output</td><td>Проверить каталог, header <code>SourceMap</code> и сетевой запрос</td><td>Ограничить публикацию карты нужной среде</td></tr><tr><td>В metafile нет объяснения</td><td>Сравниваются разные входы или другой формат отчёта</td><td>Сверить commit, command, target и схему JSON</td><td>Пересобрать baseline и candidate в одинаковых условиях</td></tr></tbody></table></div><h2>Пример с динамическим импортом</h2><p>Представим страницу поиска. В baseline она импортирует форму и таблицу при первом открытии. В candidate в общий модуль добавили визуализацию: <code>import Chart from 'chart-lib'</code>. В metafile появился новый input. Но решение зависит от маршрута импорта.</p><pre><code>// Динамическая граница загрузки. Учебный пример.\nconst openChart = async () => {\n const { renderChart } = await import('./chart/render-chart.js');\n return renderChart();\n};\n\nbutton.addEventListener('click', openChart);</code></pre><p>Если сборщик поддерживает code splitting и конфигурация сохраняет эту границу, библиотека может уйти в отдельный chunk. Тогда initial bundle не обязан вырасти на весь вклад библиотеки. Цена появляется при открытии графика: пользователь ждёт дополнительный запрос и выполнение кода. Нужно измерять оба пути.</p><p>Статический импорт даёт другой результат: <code>import { renderChart } from './chart/render-chart.js'</code>. Если модуль достижим из entry point и не исключён настройками, он может попасть в initial output. Это не ошибка само по себе. Для критического пути важнее время до функции, чем минимальный размер каждого файла. Сначала сформулируйте границу загрузки, затем проверьте, сохранил ли её bundler.</p><p>Динамический импорт также не гарантирует маленький chunk. Внутри него могут оказаться общие зависимости, полифиллы или набор файлов, созданный шаблонным путём. Для runtime-пути проверяйте сетевой waterfall, размер после сжатия, cache headers и время выполнения. Нельзя делать вывод только по строке <code>delta</code> в отчёте.</p><h2>Не перепутать состав с поведением</h2><p>Tree-shaking удаляет код, который инструмент считает недостижимым. Побочные эффекты, формат модуля и настройки package могут изменить этот вывод. Если большой input присутствует в отчёте, это ещё не доказывает, что весь исходный файл попал в переданный bundle. Смотрите связь input с output и фактические bytes output.</p><p>Дубликат зависимости часто выглядит как два похожих пути: одна копия разрешилась из корня, другая — из вложенного package. Сначала проверьте lockfile и resolver. Alias может уменьшить размер, но сломать пакет, который рассчитывает на другую версию или экспорт. Правило «свести всё к одной версии» применяйте только после проверки совместимости.</p><p>Source map нельзя считать частью пользовательского JavaScript без проверки HTTP. Если карта доступна по ссылке из production-ответа, браузер и инструменты разработчика смогут запросить её. Это удобно для отладки, но карта может раскрывать исходники и увеличивать доступный объём артефактов. Решение зависит от политики проекта и среды.</p><h2>Действия по порядку</h2><ol><li>Зафиксировать baseline и candidate: commit, lockfile, runtime, команда, режим, entry points, flags и output directory.</li><li>Собрать оба отчёта состава на одинаковом окружении. Записать exit code и не смешивать cold cache с warm cache без пометки.</li><li>Нормализовать пути и схему JSON. Запустить diff по <code>inputs</code>, затем отсортировать изменения по абсолютной delta.</li><li>Для каждого заметного input найти output и chunk. Отделить initial, lazy и shared части.</li><li>Проверить причину: импорт, версия зависимости, resolver, plugin, tree-shaking, minify или source map.</li><li>Сделать одно изменение. Пересобрать candidate и повторить diff, чтобы увидеть, исчезла ли именно заявленная delta.</li><li>Проверить браузерный путь: network transfer, compression, cache hit, время загрузки lazy chunk и ошибки runtime.</li><li>Зафиксировать отрицательный путь: если metafile стабилен, не менять импорт, а перейти к delivery или browser measurement.</li></ol><h2>Ограничения и критерий готовности</h2><p>Metafile показывает модель сборщика, а не полную стоимость для пользователя. Разные bundler описывают input и output по-разному. Сжатие, HTTP-кэш, CDN, service worker и скорость CPU находятся за пределами одного JSON. Source map может быть создана, но не отдана клиенту. Поэтому сравнение состава нельзя выдавать за измерение производительности страницы.</p><p>Нельзя считать исправлением постоянное отключение source map, удаление зависимости по имени или включение агрессивного split без проверки поведения. Нельзя сравнивать отчёты после разных изменений в lockfile и конфигурации. Если входы различаются, сначала восстановите сопоставимые условия; иначе отрицательный результат анализа честнее случайного вывода.</p><p>Готовность подтверждается четырьмя артефактами: отчёты baseline и candidate с условиями запуска, diff с конкретным input и output, проверка изменённого пользовательского пути и повторная сборка после действия. Другой инженер должен увидеть, что изменилось, воспроизвести проверку и понять, почему выбранное действие относится к причине. Если причина не найдена, готовым результатом считается зафиксированная граница: состав bundle стабилен, следующий тест идёт на уровне compression, HTTP или браузера.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://esbuild.github.io/api/#metafile\" target=\"_blank\" rel=\"noopener noreferrer\">esbuild API: Metafile</a> — официальный формат build metadata, включая inputs и outputs. Документация описывает инструмент, но не даёт результатов конкретного проекта.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/SourceMap\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: SourceMap HTTP header</a> — правила указания source map через HTTP. Наличие заголовка не определяет политику публикации карты в вашем production.</li></ul>"}
|