8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"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) => {\\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) => item.delta !== 0)\\n .sort((left, right) => 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 () => {\\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>"
|
||
}
|