8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"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>"
|
||
}
|