Files

8 lines
20 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 25,
"slug": "editorial-2027-04-field-build-evolution",
"title": "Рост frontend bundle: как найти конкретный input и не чинить симптом",
"excerpt": "Общий размер JavaScript-артефакта показывает симптом, но не причину. Разбираем сопоставимый diff metafile, связь input с output и проверку реальной доставки в браузер.",
"contentHtml": "<p>После изменения frontend-сборки команда видит в отчёте новый большой файл и сразу предлагает удалить самую заметную библиотеку. Это симптом, а не диагноз: такой ход часто промахивается мимо причины. Рост мог появиться из-за нового entry point (точки входа), дубликата зависимости, другой ветки разрешения пакета, изменившегося tree-shaking или публикации source map. В результате можно сломать рабочий экран, а initial-загрузка останется прежней.</p><p>Разберём воспроизводимый сценарий: baseline и candidate собраны из разных состояний проекта, а candidate стал тяжелее. Цель диагностики — не найти «виновный пакет», а показать цепочку <code>input → import → output/chunk → HTTP-ответ</code>. После этого решение уже предметное: изменить импорт, выровнять зависимость, вернуть настройку сборщика или проверить доставку. Если цепочка не сходится, честный результат — не менять код наугад.</p><h2>Что именно измеряет bundle-анализ</h2><p>Сборщик читает entry point и проходит граф импортов. Затем он преобразует модули, удаляет часть недостижимого кода, объединяет совместимые модули и записывает один или несколько output-файлов. В esbuild JSON-метаданные сборки называются <code>metafile</code>. В них есть <code>inputs</code> и <code>outputs</code>: первые описывают входные файлы и их исходный размер, вторые — созданные артефакты, их размер и вклад входов.</p><p><code>inputs[path].bytes</code> — не размер загруженного браузером файла. Это размер входного файла, участвовавшего в анализе. <code>outputs[path].bytes</code> — размер созданного output до сетевого сжатия. Для вывода о пользовательской загрузке дополнительно понадобятся фактический HTTP-ответ, gzip или Brotli, cache headers, service worker и порядок запросов. Один показатель нельзя подменять другим.</p><p>Ещё одна граница проходит между текстовой сводкой и JSON. Формат вывода команды анализа удобен для человека, но его не стоит использовать как API для автоматического diff. Для инструмента сравнения берите JSON-метаданные и явно фиксируйте версию сборщика и схему полей.</p><figure><img src=\"/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg\" alt=\"Цепочка диагностики роста JavaScript bundle: сравнение двух metafile, поиск изменившегося input, связь с output и проверка chunk, source map и сетевого ответа\" loading=\"lazy\" /><figcaption>Сначала связываем delta с конкретным input и output, затем проверяем, что пользователь действительно получил в ответе. Общий размер файла — только начало расследования.</figcaption></figure><h2>Сначала сделайте два отчёта сопоставимыми</h2><p>Diff имеет смысл только для сборок, где изменён один понятный фактор. Перед запуском сохраните commit, lockfile, версию Node.js и bundler, команду, режим production/development, entry points, флаги и каталог output. Зафиксируйте также, были ли включены minify, source map, code splitting и анализ зависимостей.</p><p>Минимальная команда из документации esbuild выглядит так:</p><pre><code>esbuild app.js --bundle --metafile=meta.json --outfile=out.js</code></pre><p>Для candidate используйте ту же команду и меняйте только заявленное условие. Если baseline собирался с <code>--outfile</code>, а candidate с <code>--outdir</code>, сравнение файлов уже смешивает изменение состава и изменение режима записи. То же происходит при замене lockfile, resolver conditions или платформы без отметки в протоколе.</p><p>Пути в metafile по умолчанию относительные. Это помогает получать воспроизводимые отчёты на разных машинах. Если один отчёт содержит <code>src/app.ts</code>, а другой — абсолютный путь рабочей станции, нормализуйте пути до сравнения или пересоберите baseline. Иначе один input будет ошибочно принят за два.</p><h2>Воспроизводимый diff по inputs</h2><p>Ниже — самостоятельный пример без зависимости от конкретного проекта. Функция берёт два объекта с полем <code>inputs</code>, добавляет нули для новых и исчезнувших путей и сортирует изменения по модулю delta. Она не утверждает, что вход целиком оказался в output: следующий шаг обязан проверить раздел <code>outputs</code>.</p><pre><code>function diffInputs(before, after) {\\n const names = new Set([\\n ...Object.keys(before.inputs || {}),\\n ...Object.keys(after.inputs || {}),\\n ]);\\n\\n return [...names]\\n .map((name) =&gt; {\\n const beforeBytes = before.inputs?.[name]?.bytes ?? 0;\\n const afterBytes = after.inputs?.[name]?.bytes ?? 0;\\n return {\\n name,\\n before: beforeBytes,\\n after: afterBytes,\\n delta: afterBytes - beforeBytes,\\n };\\n })\\n .filter((item) =&gt; item.delta !== 0)\\n .sort((left, right) =&gt; Math.abs(right.delta) - Math.abs(left.delta));\\n}\\n\\nconst baseline = {\\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};\\nconst candidate = {\\n inputs: {\\n ...baseline.inputs,\\n 'node_modules/chart-lib/index.js': { bytes: 7600 },\\n },\\n};\\n\\nconsole.log(diffInputs(baseline, candidate));\\n// [{ name: 'node_modules/chart-lib/index.js', before: 0,\\n// after: 7600, delta: 7600 }]</code></pre><p>Список с большой delta — это список кандидатов для проверки, а не готовый вердикт. Сверьте путь с lockfile и найдите первый импорт, который приводит к нему. Наличие файла в <code>inputs</code> говорит, что сборщик его читал; оно не говорит, что все его исходные байты попали в конкретный output после tree-shaking.</p><h2>Свяжите input с output и chunk</h2><p>В каждом output esbuild хранит собственное поле <code>inputs</code>. Вложенное <code>bytesInOutput</code> показывает вклад входного файла в этот output. Там же могут быть <code>imports</code>, <code>exports</code> и <code>entryPoint</code>. Эта связь отвечает на главный вопрос: новый input увеличил initial-файл, lazy chunk, общий chunk или вообще не тот артефакт, который измеряет команда.</p><p>Проверяйте путь по такой последовательности:</p><ol><li>В <code>inputs</code> найдите новые и выросшие пути, затем отсортируйте их по <code>delta</code>.</li><li>В каждом output найдите тот же input и запишите <code>bytesInOutput</code>, имя output и его <code>entryPoint</code>, если поле есть.</li><li>По <code>outputs[*].imports</code> восстановите связи между output и отделите initial-файл от импортируемого chunk.</li><li>Сопоставьте путь с исходным импортом, lockfile и настройками resolver. Проверьте, не появилось ли две версии одной библиотеки.</li><li>Соберите candidate после одного изменения и повторите diff. Исправление доказано только тогда, когда исчезла заявленная delta и пользовательский путь продолжил работать.</li></ol><p>Полезно хранить рядом с diff короткую запись: «input <code>chart-lib/index.js</code> добавлен в <code>search.js</code>, output — <code>search-ABC.js</code>, initial не изменился, lazy-запрос вырос после нажатия кнопки». Такая запись воспроизводимее, чем фраза «bundle стал меньше».</p><h2>Матрица симптомов и проверок</h2><div class=\"table-scroll\"><table><caption>Как перейти от наблюдаемого симптома к проверяемому действию</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 и entry point</td><td>Разделить загрузку или удалить импорт, если функция не нужна</td></tr><tr><td>Старый input вырос</td><td>Изменились export, plugin или transform</td><td>Настройки сборки, формат модуля и diff lockfile</td><td>Вернуть совместимую настройку и пересобрать</td></tr><tr><td>Одна библиотека имеет два пути</td><td>Две версии или разные условия resolver</td><td>Lockfile, реальные пути и package exports</td><td>Свести версии только после проверки совместимости</td></tr><tr><td>Metafile прежний, HTTP-ответ тяжелее</td><td>Изменились minify, compression или headers</td><td>Raw, gzip/Brotli, response headers и cache</td><td>Исправлять delivery, не импорт</td></tr><tr><td>Выросла карта</td><td>Source map создаётся или публикуется иначе</td><td>Каталог, <code>SourceMap</code> и сетевой запрос</td><td>Разделить политику debug-артефактов и production</td></tr><tr><td>Diff нестабилен между машинами</td><td>Разные пути, runtime или lockfile</td><td>Fingerprint окружения и относительность путей</td><td>Вернуть одинаковые условия до сравнения</td></tr></tbody></table></div><h2>Динамический import меняет место стоимости</h2><p>Рассмотрим экран поиска, где график нужен только после нажатия кнопки. Если сборщик сохраняет границу code splitting, динамический <code>import()</code> может вынести модуль в отдельный файл. Initial-загрузка тогда не обязана увеличиться на весь график, но пользователь заплатит дополнительным запросом и выполнением кода при открытии графика.</p><pre><code>const openChart = async () =&gt; {\\n const { renderChart } = await import('./chart/render-chart.js');\\n return renderChart();\\n};\\n\\ndocument.querySelector('#open-chart')\\n .addEventListener('click', openChart);</code></pre><p>В esbuild code splitting требует output directory и формат ESM. Без включённого splitting асинхронная семантика <code>import()</code> сохраняется, но импортированный код может оказаться в том же bundle. Поэтому одного наличия динамического импорта недостаточно: проверьте флаг, формат и фактический список output.</p><p>Изменение размера lazy chunk — не автоматически регресс. Если график редко открывают, меньший initial путь может быть предпочтительнее. Если его открывают сразу после первого экрана, дополнительный запрос может увеличить время до функции. Сравните оба пользовательских сценария: холодную загрузку страницы и переход по кнопке. В network panel запишите transfer size, статус cache, длительность запроса и ошибки выполнения.</p><h2>Почему нельзя удалять код по имени</h2><p>Tree-shaking удаляет недостижимые объявления, но его результат зависит от статических ES-модулей и побочных эффектов. В документации esbuild отдельно указано, что tree-shaking использует <code>import</code>/<code>export</code>, а CommonJS не даёт такой же статической информации. Поэтому «большой пакет» может быть не причиной, а следствием выбранного формата модуля или настройки package fields.</p><p>То же относится к дубликатам. Alias или принудительное сведение версий иногда уменьшает output, но может изменить API, side effects или поведение плагина. Сначала установите, какие два пути разрешаются и какие exports реально используются. Потом проверьте тестовый сценарий и только затем меняйте resolver.</p><p>Source map — отдельный артефакт от JavaScript. Заголовок HTTP <code>SourceMap</code> указывает браузерным инструментам, где искать карту для оптимизированного ресурса. В production нужно проверить не только наличие файла в каталоге, но и ссылку из ответа, права доступа и принятую в проекте политику раскрытия исходников. Рост карты не доказывает рост пользовательского JavaScript, а её публикация может расширить доступный набор исходных материалов.</p><h2>Критерий готовности и границы метода</h2><p>Диагностика готова, когда другой инженер получает два отчёта с одинаковыми условиями, diff с конкретным input, связь этого input с output/chunk, результат браузерной проверки и одно изменение, которое можно повторить. Для каждой цифры должно быть ясно, это входные bytes, raw output или сетевой transfer. Для каждого решения должна быть названа цена: дополнительный lazy-запрос, риск несовместимости версий, потеря отладки или изменение initial-критического пути.</p><p>Metafile не измеряет время выполнения JavaScript, стоимость парсинга и компиляции, порядок запросов, cache hit или работу service worker. Формат поля зависит от bundler и его версии; код, который жёстко ожидает только текущие поля, следует защищать проверкой схемы. Динамический импорт не гарантирует отдельный chunk без подходящей конфигурации. Сжатие может изменить порядок «самых больших» файлов после raw diff.</p><p>Остановите расследование и вернитесь к сборке, если baseline и candidate отличаются lockfile, entry points или режимом без документированной причины. Не называйте результатом «оптимизацию» удаление зависимости по названию. Если состав output не изменился, переходите к compression, HTTP, кэшу и измерению браузерного пути. Именно эта граница не даёт исправить не тот слой.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://esbuild.github.io/api/#metafile\" target=\"_blank\" rel=\"noopener noreferrer\">Официальная документация esbuild: Metafile</a> — схема <code>inputs</code>/<code>outputs</code>, поля <code>bytes</code>, <code>bytesInOutput</code>, связи импортов и оговорка о путях.</li><li><a href=\"https://esbuild.github.io/api/#splitting\" target=\"_blank\" rel=\"noopener noreferrer\">Официальная документация esbuild: Code splitting</a> — условия для splitting и поведение асинхронного <code>import()</code>.</li><li><a href=\"https://github.com/evanw/esbuild/blob/main/docs/architecture.md#code-splitting\" target=\"_blank\" rel=\"noopener noreferrer\">Официальное архитектурное описание esbuild</a> — связь tree-shaking, entry points, shared и lazy chunks.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/SourceMap\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: HTTP-заголовок SourceMap</a> — назначение заголовка и его приоритет над source annotation.</li></ul>"
}