8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 20,
|
||
"slug": "editorial-2027-06-mechanism-performance-capstone",
|
||
"title": "Waterfall без иллюзий: как доказать, что ресурс задерживает первый экран",
|
||
"excerpt": "Длинная полоса в waterfall не равна причине задержки. Разбираем зависимость ресурса, инициатора и потребителя, а затем проверяем одно изменение на учебной странице.",
|
||
"contentHtml": "<p>Пользователь видит пустой или неполный первый экран. В DevTools рядом с ним растягивается полоса шрифта или изображения. Команда объявляет этот ресурс виновником и меняет порядок загрузки. Через релиз экран не ускоряется, зато появляется лишний preload, вспышка нестилизованного текста или гонка между скриптами. Цена ошибки — не только потерянные миллисекунды. Вы меняете контракт загрузки, не доказав, что страницу действительно задерживал этот запрос.</p>\n<p>Waterfall показывает время сетевой работы, но не причинность. Ресурс влияет на экран только тогда, когда его результат нужен конкретной зависимости: обычный внешний script может приостановить разбор HTML, рендер может ждать таблицу стилей, layout — шрифт, а компонент — данные. Поэтому ищите не самую длинную полосу, а цепочку «инициатор → ресурс → потребитель → наблюдаемый эффект».</p>\n<h2>Причина начинается с потребителя</h2>\n<p>У записи ресурса есть временная и причинная часть. <code>startTime</code> показывает начало работы запроса, а <code>responseEnd</code> — момент, когда браузер получил последний байт. <code>initiatorType</code> описывает тип инициатора: например, <code>link</code> или <code>img</code> для HTML-элемента, <code>css</code> для URL из CSS, <code>script</code> для загрузки скрипта и <code>fetch</code> для Fetch API. Эти поля фиксируют наблюдение, но не сообщают, ждал ли результат первый экран.</p>\n<p>Для причинности ответьте на три вопроса. Какой элемент или код потребляет ответ? В какой момент он потребляет его? Что изменится, если ответ придёт позже или не придёт вовсе? Если на третий вопрос нет проверяемого ответа, запись остаётся кандидатом. Её нельзя называть узким местом.</p>\n<p>Это различие удобно представить как контракт. Инициатор объясняет, откуда взялся запрос. Ресурс объясняет, что приехало и когда. Потребитель объясняет, какая операция не может продолжиться без результата. Наблюдаемый эффект связывает техническую цепочку с пользовательским симптомом: поздним текстом, пустыми карточками, скачком высоты или недоступной кнопкой.</p>\n<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>CSS заканчивается поздно, первый экран пустой</td><td>Стили нужны до первого визуального результата</td><td>Сопоставить тег link, DOM и момент появления элемента</td><td>Уменьшить критический CSS или разделить его, затем проверить layout shift</td></tr><tr><td>Script начинается рано и долго выполняется</td><td>Разбор или главный поток ждёт выполнение</td><td>Проверить атрибуты, инициатор и long task</td><td>Применить defer или разбить работу после проверки зависимостей</td></tr><tr><td>Шрифт имеет длинную полосу, но контент уже виден</td><td>Шрифт не нужен первому экрану или заменяется fallback</td><td>Сравнить ответ шрифта с визуальным результатом и layout</td><td>Не добавлять preload; проверить font-display и потребителя</td></tr><tr><td>JSON заканчивается поздно, карточки пустые</td><td>Компонент ждёт fetch</td><td>Найти вызов, состояние ожидания и момент отображения данных</td><td>Оптимизировать запрос или состояние загрузки</td></tr><tr><td>Запись неполная или отсутствует</td><td>Нет поддержки поля, ограничен доступ или очищен буфер</td><td>Проверить браузер, Timing-Allow-Origin и момент чтения</td><td>Пометить «нет данных» и выбрать другой сигнал</td></tr></tbody></table></div>\n<figure><img src=\"/assets/editorial/2027/performance-capstone-2027-evidence-boundary-matrix.svg\" alt=\"Матрица проверки ресурса: инициатор, время, статус блокировки, потребитель и действие\" loading=\"lazy\" /><figcaption>Строка waterfall становится доказательством только после связи с потребителем. Размер ресурса — один из входов, а не итоговый вердикт.</figcaption></figure>\n<h2>Как браузер строит критический путь</h2>\n<p>Рассмотрим страницу с таким HTML:</p>\n<pre><code><head>\n <link rel='stylesheet' href='/app.css'>\n <script src='/vendor.js' defer></script>\n <script src='/analytics.js' async></script>\n</head>\n<body>\n <main id='catalog'></main>\n <script src='/catalog.js' defer></script>\n</body></code></pre>\n<p><code>app.css</code> может задержать отображение стилизованного содержимого. <code>vendor.js</code> и <code>catalog.js</code> загружаются во время разбора HTML, а классические скрипты с <code>defer</code> выполняются после разбора и сохраняют порядок между собой. <code>analytics.js</code> с <code>async</code> выполняется после загрузки независимо от порядка других скриптов. Поэтому он подходит для независимой аналитики, но не для кода, которому нужен ещё не созданный глобальный объект.</p>\n<p>У этой схемы есть два разных пути. CSS и отложенные скрипты могут влиять на то, когда компонент получит DOM и стили. Аналитика может иметь длинную сетевую полосу и не участвовать в рендере вовсе. Если убрать её из учебной копии и первый экран не изменится, полоса была коррелятом, а не причиной.</p>\n<p>Проверяйте не только загрузку, но и работу главного потока. Быстрый ответ сервера не помогает, если после него выполняется длинная синхронная задача. И наоборот, поздний ресурс может не менять картинку, если компонент уже показал полезный fallback. В обоих случаях ответ находится на границе ресурса и его потребителя.</p>\n<h2>Минимальный воспроизводимый замер</h2>\n<p>Учебный наблюдатель ниже запускается в консоли локальной или тестовой страницы после загрузки. Он не меняет сеть и не утверждает, что каждая ранняя запись блокирует экран. Полный <code>url</code> сохраняется намеренно: удаление query-параметров может смешать две разные версии ответа.</p>\n<pre><code>const navigation = performance.getEntriesByType('navigation')[0];\nconst domEnd = navigation?.domContentLoadedEventEnd ?? Infinity;\n\nconst resources = performance\n .getEntriesByType('resource')\n .filter((entry) => entry.startTime < domEnd)\n .map((entry) => ({\n url: entry.name,\n initiator: entry.initiatorType || 'unknown',\n start: Math.round(entry.startTime),\n end: Math.round(entry.responseEnd),\n duration: Math.round(entry.duration),\n blocking: entry.renderBlockingStatus ?? 'unknown',\n }));\n\nconst firstContentfulPaint = performance\n .getEntriesByType('paint')\n .find((entry) => entry.name === 'first-contentful-paint');\n\nconsole.table(resources);\nconsole.table({\n domContentLoaded: navigation?.domContentLoadedEventEnd ?? 'unknown',\n firstContentfulPaint: firstContentfulPaint?.startTime ?? 'unknown',\n});</code></pre>\n<p>Фильтр ограничивает список ресурсами, которые начали работу до <code>DOMContentLoaded</code>. Это удобная граница для первичного поиска, но не доказательство готовности экрана: DOM мог закончить разбор, пока изображение, шрифт или отрисовка ещё продолжаются. <code>first-contentful-paint</code> фиксирует первое отображение текста или изображения, но не подтверждает, что пользователь увидел нужный блок. Если запись недоступна, сохраняйте <code>unknown</code> и измеряйте выбранный элемент браузерным сценарием.</p>\n<p>Дальше найдите инициатор в HTML или исходном коде. Для записи, инициированной HTML-элементом, проверьте тег. Для <code>script</code> найдите вызов fetch, импорт или создание элемента. Для <code>css</code> проверьте правило и используемый шрифт. Затем назовите потребителя. «CSS заканчивается поздно» — описание записи. «Карточки не получают стили до первого layout, потому что link указывает на полный файл» — проверяемая гипотеза.</p>\n<h2>Проверка гипотезы одним изменением</h2>\n<ol><li>Зафиксируйте URL, браузер, viewport, сеть, режим кэша и состояние авторизации. Без этих условий две записи waterfall нельзя честно сравнить.</li><li>Опишите симптом словами пользователя: пустой первый экран, поздние карточки, скачок текста или задержка интерактивности. Запишите момент и элемент, а не только общую длительность.</li><li>Снимите navigation entry, resource entries и выбранный paint-сигнал в одном прогоне. Сохраните исходные значения, URL, инициатор, время и доступный статус блокировки.</li><li>Для ранней записи найдите конкретный тег, вызов или CSS-правило, которое запустило запрос. Затем назовите потребителя и его зависимость от ответа.</li><li>Отложите ресурс, замените ответ заглушкой или удалите его только в учебной копии. Если симптом не меняется, гипотеза не подтверждена; не исправляйте production по одной полосе.</li><li>Измените один фактор: <code>defer</code>, разделение CSS, порядок запроса или код потребителя. Не смешивайте изменение сети с изменением рендера.</li><li>Повторите прогон в тех же условиях. Сравните визуальный критерий, first contentful paint, long tasks и нужный участок waterfall. Сокращение отдельной полосы без изменения симптома не считается успехом.</li></ol>\n<p>Полезен и отрицательный контроль. Если вы проверяете гипотезу о шрифте, повторите прогон с тем же CSS, но с локальным fallback. Если проверяете JSON, оставьте тот же ответ и задержите только его доставку. Контроль должен менять ровно один фактор; иначе вы не узнаете, что именно повлияло на результат.</p>\n<h2>Почему популярные исправления дают обратный эффект</h2>\n<p><code>preload</code> запускает запрос раньше, но не делает ресурс автоматически критичным. Лишняя предварительная загрузка конкурирует с HTML, CSS и данными. Укажите правильный <code>as</code>, проверьте совпадение URL и убедитесь, что документ действительно использует ответ. Иначе браузер скачает ресурс, который не изменит экран.</p>\n<p><code>async</code> не гарантирует порядок выполнения. Это преимущество для независимого кода и источник гонок для зависимого. <code>defer</code> сохраняет порядок классических внешних скриптов и переносит их выполнение после разбора документа, но не уменьшает размер файла и не сокращает время работы главного потока.</p>\n<p>Разделение CSS снижает ранний объём только при точной границе. Ошибка даёт FOUC, неверный порядок правил или layout shift. Отложенный шрифт может убрать часть ранней работы, но изменить переносы строк и высоту блока. Оценивайте исправление по тому же потребителю, который породил исходный симптом.</p>\n<p>Если waterfall показывает поздний запрос, но первый экран от него не зависит, остановите оптимизацию этой строки. Если замер меняет состояние кэша, не сравнивайте его с холодным запуском. Если данные недоступны из-за политики браузера, не подставляйте ноль. Вывод «причина не доказана» точнее уверенной правки не того слоя.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Resource Timing зависит от браузера и политики доступа. Кросс-доменные значения могут быть скрыты без <code>Timing-Allow-Origin</code>. <code>renderBlockingStatus</code> и paint-записи могут отсутствовать в конкретной реализации. <code>DOMContentLoaded</code> не равен FCP или LCP, а локальная сеть не воспроизводит мобильное устройство. Записи Performance API не описывают серверную очередь, весь главный поток и пользовательский опыт одной цифрой.</p>\n<p>Учебный код показывает форму расследования, а не production-результат. Для реального решения нужны повторяемые прогоны в целевых браузерах, сохранённые условия и выбранный визуальный критерий. DevTools помогает найти кандидата; доказательство появляется после контролируемого изменения и повторного наблюдения.</p>\n<p>Работа готова, когда для симптома есть исходная запись и повторяемый критерий, у спорного ресурса найдены инициатор и потребитель, одно изменение прошло в тех же условиях, а отрицательный путь описан. После изменения должен измениться именно наблюдаемый симптом или заранее выбранная метрика. Если меняется только длина полосы, расследование не закончено.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.w3.org/TR/navigation-timing-2/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Navigation Timing Level 2</a> — интерфейс навигационных временных меток, включая <code>domContentLoadedEventEnd</code>. Это рабочий черновик: он описывает API, но не сообщает значения метрик конкретного сайта.</li><li><a href=\"https://www.w3.org/TR/resource-timing/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Resource Timing</a> — записи ресурсов, <code>startTime</code>, <code>responseEnd</code>, <code>initiatorType</code> и <code>renderBlockingStatus</code>. Спецификация не доказывает причинность задержки и ограничивает часть полей политикой доступа.</li><li><a href=\"https://html.spec.whatwg.org/multipage/scripting.html#the-script-element\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Standard: script element</a> — нормативная модель обычных, <code>async</code> и <code>defer</code>-скриптов. Реальное влияние зависит от зависимостей кода и работы главного потока.</li><li><a href=\"https://html.spec.whatwg.org/multipage/links.html#link-type-preload\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Standard: preload</a> — правила предварительной загрузки и назначения ресурса через <code>as</code>. Спецификация не гарантирует выигрыш без проверки потребителя и конкуренции за сеть.</li><li><a href=\"https://www.w3.org/TR/paint-timing/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Paint Timing</a> — определения записей <code>first-paint</code> и <code>first-contentful-paint</code>. Это ориентир начала отображения, а не доказательство готовности конкретного блока или всего первого экрана.</li></ul>"
|
||
}
|