Files
progcode/editorial/agent-rewrites/027.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 KiB
JSON
Raw 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": 27,
"slug": "editorial-2027-04-practice-build-evolution",
"title": "Сборка быстрее на 20%? Сначала докажите, что вход одинаковый",
"excerpt": "Как сравнивать frontend-сборки по одному входу, не принимать cache hit за ускорение и находить причину роста bundle по данным артефакта.",
"contentHtml": "<p>Новая frontend-сборка закончилась за 38 секунд вместо 47. Через день CI снова показывает 47 секунд. В другом запуске candidate оказался быстрее, но собирал только production entry, а baseline — два entry и source map. Цена ошибки — неверный выбор инструмента, потерянное время на миграцию и артефакт, который нельзя сравнить с опубликованным.</p>\n<p><strong>Тезис:</strong> время и размер имеют смысл только для одинаковой работы. Сборщик получает исходный граф, lockfile, конфигурацию, runtime, entry points и состояние кэша. Если хотя бы один существенный вход отличается, результат нужно пометить как несопоставимый, а не объявлять победителя.</p>\n<h2>Что именно сравнивает инженер</h2>\n<p>Сборка не является одной операцией. Сначала резолвер строит граф модулей. Затем плагины и loaders преобразуют входы. Bundler раскладывает граф по chunks, минифицирует код и пишет output. Кэш может вернуть промежуточный результат до части этих шагов. Поэтому число из секундомера описывает не «скорость инструмента», а конкретный маршрут с конкретным состоянием.</p>\n<p>Размер тоже имеет несколько значений. Размер исходного input показывает вклад модуля в сборку. Размер output показывает файл на диске. Transfer size показывает объём после compression и HTTP-обмена. Эти величины нельзя подменять друг другом. Большой input может попасть в отложенный chunk, а небольшой модуль — блокировать первый экран.</p>\n<figure><img src=\"/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg\" alt=\"Последовательность сравнения сборок: одинаковый вход, конфигурация, cache state, замер и проверка артефакта\" loading=\"lazy\" /><figcaption>Секундомер запускается после фиксации условий. Если меняется вход или конфигурация, сравнение начинается заново.</figcaption></figure>\n<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>Candidate быстрее в одном запуске</td><td>У него тёплый cache</td><td>Сравнить hit/miss, directory и серию cold/warm</td><td>Развести режимы и повторить замер</td></tr><tr><td>Output меньше, но entry другой</td><td>Собирается другая работа</td><td>Сверить entry, mode, flags и список chunks</td><td>Исключить запуск или выровнять вход</td></tr><tr><td>Bundle вырос на 60 КБ</td><td>Новый input или duplicate dependency</td><td>Сравнить metafile inputs и lockfile</td><td>Проверить import, версии и split</td></tr><tr><td>Fingerprint совпал, результат старый</td><td>В ключ не вошёл plugin или linked package</td><td>Изменить один вход и проверить invalidation</td><td>Расширить ключ и проверить output после hit</td></tr></tbody></table>\n<h2>Кэш повторяет не проект, а функцию от входов</h2>\n<p>Кэш хранит результат, полученный при определённых условиях. Его ключ должен различать изменения, которые влияют на граф, transform или output. Для типового frontend-проекта это lockfile, нормализованная конфигурация, версия Node и bundler, исходный digest, entry и параметры режима. Состав полей зависит от инструмента. Нельзя скопировать ключ webpack в Vite и считать его полным.</p>\n<p>Неполный ключ даёт опасный cache hit. Например, команда меняет alias или plugin, но имя cache namespace остаётся прежним. Bundler видит старый промежуточный результат и выпускает артефакт без нового правила. Постоянный флаг принудительной пересборки скрывает проблему, но не объясняет, что именно должно инвалидировать кэш.</p>\n<p>Слишком широкий ключ создаёт обратную проблему. Если в него попадает абсолютный путь временной директории или случайный идентификатор job, каждый запуск выглядит новым. CI теряет повторяемость. Поэтому ключ должен быть детерминированным: одинаковые значимые входы дают одинаковое значение, а изменение значимого входа меняет его.</p>\n<h2>Учебная проверка сопоставимости</h2>\n<p>Ниже учебный код. Он не запускает bundler и не измеряет реальный проект. Функция получает два уже записанных запуска, отбрасывает разные inputFingerprint и только затем считает разницу. Числа нужны для показа контракта, а не для заявления о production-эффекте.</p>\n<pre><code>function compareBuildRuns({ baseline, candidate }) {\n if (!baseline || !candidate) {\n return { comparable: false, reason: 'нет двух запусков' };\n }\n\n if (baseline.inputFingerprint !== candidate.inputFingerprint) {\n return { comparable: false, reason: 'входы сборки различаются' };\n }\n\n return {\n comparable: true,\n deltaMs: candidate.durationMs - baseline.durationMs,\n deltaBytes: candidate.outputBytes - baseline.outputBytes,\n };\n}\n\nconst baseline = {\n inputFingerprint: 'src-42', durationMs: 420, outputBytes: 180000,\n};\nconst candidate = {\n inputFingerprint: 'src-42', durationMs: 380, outputBytes: 176000,\n};\nconst changedInput = {\n inputFingerprint: 'src-43', durationMs: 350, outputBytes: 174000,\n};\n\nconsole.log(compareBuildRuns({ baseline, candidate }));\n// { comparable: true, deltaMs: -40, deltaBytes: -4000 }\nconsole.log(compareBuildRuns({ baseline, candidate: changedInput }));\n// { comparable: false, reason: 'входы сборки различаются' }</code></pre>\n<p>Первый вызов разрешает вычисление: учебный candidate завершился на 40 мс раньше и дал на 4000 байт меньше. Второй вызов останавливается до сравнения цифр. Более быстрое число не компенсирует другой исходный граф. В рабочем отчёте fingerprint должен быть связан с commit, lockfile, entry и версией окружения, а не с короткой строкой, которую никто не умеет восстановить.</p>\n<h2>Почему одного запуска недостаточно</h2>\n<p>Время зависит от cache state, нагрузки CPU, диска и фоновых процессов. Поэтому записывайте cold и warm отдельно. Для каждой серии сохраняйте несколько запусков и выбирайте заранее заданное представление: медиану для типичного времени или p95 для хвоста. Не смешивайте установку зависимостей с bundling, если вопрос касается только сборки.</p>\n<p>Сравнивайте не только duration. Запишите exit code, peak memory, число и имена chunks, output bytes, cache hit/miss и команду запуска. Если candidate быстрее, но потерял source map или собрал меньше entry, это не оптимизация. Это изменение результата.</p>\n<p>В webpack contenthash помогает увидеть, какой файл изменился после изменения содержимого. Deterministic module ids уменьшают случайные изменения имён. Эти настройки улучшают диагностику и кэширование, но не делают разные конфигурации одинаковыми. Их эффект нужно проверять на конкретном output.</p>\n<h2>От общего роста bundle к конкретному input</h2>\n<p>Размер bundle — только симптом. Сравните два metafile или эквивалентных отчёта сборщика. В JSON-метафайле esbuild можно найти inputs и их вклад в outputs. Отсортируйте delta по каждому input. Новый крупный модуль, выросший старый модуль и две версии одной зависимости ведут к разным действиям.</p>\n<p>Если delta появилась в библиотеке, найдите import path и проверьте tree-shaking. Если появились два пути к разным версиям пакета, проверьте lockfile и resolver. Если input не изменился, а output вырос, ищите plugin transform, target, minify и split. После исправления повторите сборку на том же fingerprint.</p>\n<p>Metafile не измеряет браузерную скорость. Для пользовательского эффекта отдельно смотрите transfer size, compression, cache и timing критического ресурса. Source map помогает связать bundle с исходным модулем, но карта может быть большой и не должна случайно попасть в production delivery.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Запишите вопрос сравнения: время bundling, размер output или скорость критического пути. Не смешивайте эти метрики.</li><li>Зафиксируйте commit, lockfile, entry points, mode, flags, target, версии Node и bundler, runner и расположение кэша.</li><li>Соберите baseline и candidate с одинаковой командой. Отдельно пометьте cold и warm state, сохраните raw output и exit code.</li><li>Проверьте fingerprint и состав результата. Разный fingerprint, entry, chunk или режим означает «несопоставимо», даже если число лучше.</li><li>Сравните серию запусков, chunks и input delta. Для роста найдите import path, dependency version или transform до изменения кода.</li><li>После изменения повторите измерение на том же входе. Затем отдельно проверьте transfer и критический пользовательский маршрут.</li></ol>\n<h2>Ограничения</h2>\n<p>Учебная функция не знает, какие поля использует ваш bundler. Fingerprint не доказывает корректность, если его строит неполный скрипт. Одинаковый runtime не устраняет различия диска, CPU и виртуализации. Число запусков не исправляет несопоставимый entry.</p>\n<p>Рост output не равен росту времени выполнения в браузере. Source map и metafile описывают артефакт, но не гарантируют cache hit у пользователя. Compression, CDN, service worker и код до первого экрана требуют отдельных наблюдений. Не называйте локальную разницу production-результатом без измерения соответствующего пути.</p>\n<p>Отрицательный путь должен быть явным. Если входы различаются, функция возвращает «несопоставимо». Если output изменился, а причина не найдена, не откатывайте код по одной цифре. Если cache hit дал старый артефакт, исправьте ключ или invalidation. Не оставляйте постоянный force.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Сравнение готово, если другой инженер может восстановить два запуска по commit, lockfile, команде и окружению, увидеть одинаковый fingerprint и получить те же поля отчёта. В отчёте видны cold/warm state, серия времени, chunks, input delta и ограничения метрики.</p>\n<p>Для кэша готовность дополнительно подтверждает отрицательный тест: изменение lockfile, конфигурации, runtime и одного исходного модуля меняет ключ или приводит к зафиксированному invalidation. После cache hit output соответствует тому же входу. Только тогда разницу времени можно обсуждать как свойство проверенного маршрута.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://webpack.js.org/guides/caching/\" target=\"_blank\" rel=\"noopener noreferrer\">webpack: Caching</a> — официальное руководство описывает contenthash и связь изменения содержимого с именем output. Оно не подтверждает скорость конкретного проекта.</li><li><a href=\"https://esbuild.github.io/api/#metafile\" target=\"_blank\" rel=\"noopener noreferrer\">esbuild API: Metafile</a> — официальная документация описывает JSON-метафайл для анализа inputs и outputs. Он не заменяет измерение browser delivery.</li><li><a href=\"https://vite.dev/guide/dep-pre-bundling.html\" target=\"_blank\" rel=\"noopener noreferrer\">Vite Guide: Dependency Pre-Bundling</a> — официальное руководство перечисляет входы, влияющие на повторную оптимизацию зависимостей. Эти правила относятся к Vite dependency optimizer и не являются универсальным ключом для любого bundler.</li></ul>"
}