Files
progcode/editorial/agent-rewrites/021.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

2 lines
19 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":21,"slug":"editorial-2027-06-practice-performance-capstone","title":"Медленная первая загрузка: отделяем TTFB от блокирующих ресурсов","excerpt":"Как разобрать пустой первый экран на измеряемые участки: ожидание сервера, передача HTML и блокировка CSS или JavaScript.","contentHtml":"<p>Пользователь открывает страницу и несколько секунд видит пустой экран. В отчёте появляется одна цифра: «загрузка заняла 2,4 секунды». Этой цифры недостаточно для решения. 1,6 секунды могли уйти до первого байта HTML, а могли — на выполнение скрипта после ответа сервера. Цена ошибки — менять JavaScript, когда тормозит backend, или добавлять кеш, когда браузер ждёт блокирующий CSS.</p><p>Тезис статьи простой: сначала разделите критический путь на участки, затем меняйте один участок и повторяйте тот же замер. TTFB (time to first byte) показывает ожидание первого байта ответа. Он не показывает время до готового экрана. Передача HTML, CSS, JavaScript и работа главного потока требуют отдельных наблюдений.</p><h2>Механизм: один экран, несколько причин</h2><p>Браузер начинает навигацию с запроса. Сервер формирует ответ и отправляет первый байт. Только после этого браузер получает весь HTML и строит DOM. Когда parser встречает таблицу стилей, он ждёт CSSOM для расчёта стилей. Синхронный <code>script</code> может остановить parser. Отложенный код продолжает занимать главный поток даже после завершения сетевой загрузки.</p><p>Поэтому время нужно разложить. Участок до первого байта относится к сети, proxy и серверному обработчику. Участок от первого байта до конца ответа относится к размеру HTML и передаче. Ранний CSS влияет на построение стилей. Скрипт может задержать DOM, layout или обработку пользовательского ввода. Одна общая длительность скрывает владельца и действие.</p><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>TTFB стабильно выше 300 мс</td><td>Сервер или зависимость задерживает начало ответа</td><td>Сравнить <code>responseStart - requestStart</code> и журнал handler</td><td>Профилировать серверный путь; не начинать с bundle</td></tr><tr><td>TTFB нормален, HTML приходит долго</td><td>Большой ответ или медленная передача</td><td>Сравнить <code>responseEnd - responseStart</code> и размер HTML</td><td>Уменьшить ответ, проверить сжатие и кеш</td></tr><tr><td>HTML пришёл, первый экран ждёт CSS</td><td>Ранний stylesheet блокирует построение стилей</td><td>Сверить waterfall, initiator и видимый результат</td><td>Сократить критический CSS или отложить второстепенный</td></tr><tr><td>Сеть закончилась, экран не готов</td><td>Долгая задача на главном потоке</td><td>Посмотреть long tasks и длительность script</td><td>Разбить работу или перенести некритичную часть</td></tr><tr><td>После async ломается интерфейс</td><td>Код потерял порядок инициализации</td><td>Проверить зависимости скриптов и ошибки консоли</td><td>Вернуть порядок или использовать <code>defer</code>, если он подходит</td></tr><tr><td>Одно измерение лучше остальных</td><td>Сработал кеш или изменились сеть и устройство</td><td>Повторить серию и сравнить p50/p95</td><td>Не принимать единичный прогон за результат</td></tr></tbody></table><figure><img src='/assets/editorial/2027/performance-capstone-2027-critical-path-contract.svg' alt='Критический путь первой загрузки: запрос, TTFB, HTML, CSS и JavaScript' loading='lazy' /><figcaption>Схема разделяет ожидание ответа, получение HTML и работу ресурсов. Каждому участку нужен собственный замер и собственное действие.</figcaption></figure><h2>Учебный пример: серверная задержка видна отдельно</h2><p>Ниже — минимальный локальный сервер. Параметр <code>mode=slow</code> добавляет задержку перед отправкой заголовков. Пример намеренно проверяет только серверный участок. Он не моделирует мобильную сеть, кеш браузера, CDN, рендеринг или реальную нагрузку. Его вывод нельзя выдавать за production-результат.</p><pre><code>import { createServer } from 'node:http'; import { performance } from 'node:perf_hooks'; const server = createServer((request, response) =&gt; { const url = new URL(request.url, 'http://127.0.0.1'); const delay = url.searchParams.get('mode') === 'slow' ? 300 : 0; setTimeout(() =&gt; { response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); response.end('&lt;main&gt;ready&lt;/main&gt;'); }, delay); }); server.listen({ host: '127.0.0.1', port: 0 }, async () =&gt; { const { port } = server.address(); for (const mode of ['fast', 'slow']) { const started = performance.now(); const response = await fetch('http://127.0.0.1:' + port + '/?mode=' + mode); await response.text(); console.log(mode, Math.round(performance.now() - started), response.status); } server.close(); });</code></pre><p>Ожидаемый результат — две строки со статусом <code>200</code>. Строка <code>slow</code> должна быть примерно на 300 миллисекунд длиннее. Точное значение зависит от машины, поэтому сравнивайте режимы в одном запуске. Если различия нет, проверьте URL, единицы времени и то, что задержка стоит до <code>writeHead</code>, а не после отправки ответа.</p><p>Этот пример показывает причинность только для TTFB. Клиент ждёт тело целиком, поэтому его общее время включает передачу HTML. Чтобы измерить участки в браузере, откройте ту же страницу и прочитайте navigation entry. Не переносите число из Node в вывод о FCP или LCP: серверный пример не видит отрисовку.</p><h2>Читаем Navigation Timing</h2><p>В браузере найдите запись типа <code>navigation</code>. Поля <code>requestStart</code>, <code>responseStart</code> и <code>responseEnd</code> дают точки для разделения запроса, первого байта и конца ответа. Поле <code>domContentLoadedEventEnd</code> показывает завершение соответствующего события. Это диагностические временные точки, а не готовая оценка качества экрана.</p><pre><code>const navigation = performance.getEntriesByType('navigation')[0]; if (navigation) { console.table({ ttfb: navigation.responseStart - navigation.requestStart, html: navigation.responseEnd - navigation.responseStart, domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime }); }</code></pre><p>Если <code>responseStart - requestStart</code> велик, ищите серверную задержку, соединение и proxy. Если TTFB мал, а <code>responseEnd - responseStart</code> велик, проверьте размер HTML, сжатие и сеть. Если оба участка малы, но пользователь всё ещё видит пустой экран, переходите к ресурсам и главному потоку. Такой отрицательный путь важен: отсутствие серверной проблемы не доказывает, что страница быстрая.</p><p><code>DOMContentLoaded</code> нельзя называть временем готовности экрана. Событие связано с разбором документа и отложенными скриптами. Изображения, шрифты, layout, paint и работа JavaScript могут продолжаться. Для визуального симптома нужен отдельный наблюдаемый критерий. Если команда использует FCP или LCP, измеряйте его тем же браузерным сценарием и не подменяйте его TTFB.</p><h2>Ресурсы и порядок выполнения</h2><p>После navigation entry соберите записи ресурсов. Для каждой записи важны URL без секретных параметров, тип ресурса, время начала, конец ответа, initiator и размер. Сначала ищите ресурс, который действительно пересекается с критическим участком. Длинная полоса в waterfall сама по себе не доказывает блокировку: шрифт мог начать загрузку после отрисовки главного содержимого.</p><p>Обычный stylesheet влияет на построение стилей. Синхронный script может остановить parser. <code>defer</code> оставляет порядок отложенных скриптов и запускает их после разбора документа. <code>async</code> запускает скрипт по готовности и меняет порядок. Если второй файл использует глобальный объект первого, безоговорочная замена на <code>async</code> создаёт отрицательный путь: сеть стала быстрее, но приложение упало до инициализации.</p><p>Поле <code>renderBlockingStatus</code> полезно только при поддержке конкретного браузера и конкретной записи. Отсутствующее значение означает отсутствие данных, а не доказательство, что ресурс не блокирует. Сверяйте API с HTML и визуальным результатом. Если сведения не совпадают, оставьте это ограничение в отчёте и опирайтесь на поддерживаемые наблюдения.</p><table><caption>Как выбрать следующее изменение</caption><thead><tr><th scope='col'>Наблюдение</th><th scope='col'>Первое изменение</th><th scope='col'>Риск</th></tr></thead><tbody><tr><td>Высокий TTFB при разных клиентах</td><td>Профилировать handler и его зависимости</td><td>Изменение кеша скроет, но не устранит причину</td></tr><tr><td>Большой HTML при нормальном TTFB</td><td>Проверить структуру ответа и сжатие</td><td>Сложнее кеширование и отладка шаблона</td></tr><tr><td>CSS задерживает первый полезный контент</td><td>Отделить критические стили от второстепенных</td><td>FOUC и рассинхрон стилей</td></tr><tr><td>Script создаёт длинную задачу</td><td>Разбить вычисление или отложить его</td><td>Изменится порядок состояния и событий</td></tr></tbody></table><h2>Порядок проверки</h2><ol><li>Зафиксировать симптом: URL, устройство, браузер, сеть, режим кеша и видимый момент задержки.</li><li>Повторить страницу серией прогонов, а не одним открытием. Сохранить сырые navigation и resource entries.</li><li>Разделить TTFB, передачу HTML и время после получения документа.</li><li>Сопоставить каждый участок с серверным журналом, waterfall и главным потоком браузера.</li><li>Выбрать одну гипотезу и изменить только её: handler, HTML, CSS, script или порядок ресурса.</li><li>Повторить тот же сценарий и сравнить медиану и p95. Проверить, что визуальный симптом изменился вместе с измеряемым участком.</li><li>Проверить отрицательный путь: после изменения async/defer открыть страницу с медленной сетью и убедиться, что зависимости не запускаются в неверном порядке.</li><li>Зафиксировать ограничения и вернуть изменение, если улучшилась одна цифра, но ухудшился первый экран или интерактивность.</li></ol><h2>Ограничения</h2><p>Navigation Timing описывает события навигации, но не знает архитектуру backend и не устанавливает пороги качества. Resource Timing может скрывать часть сведений из-за политики приватности и кросс-доменных ограничений. Браузеры различаются по поддержке отдельных полей. Поэтому один API не заменяет сетевой журнал, профиль главного потока и визуальный замер.</p><p>Локальный сервер с фиксированной задержкой проверяет ветвление диагностики, но не показывает распределение latency, холодный кеш, CDN, TLS, балансировщик и конкуренцию запросов. Числа из примера не являются обещанием для production. Полевой вывод требует зафиксированных условий и серии наблюдений на целевом устройстве.</p><p>Ускорение одного участка может ухудшить другой. Отложенный CSS уменьшает блокировку, но может вызвать вспышку нестилизованного контента. Разбиение JavaScript сокращает длинную задачу, но увеличивает количество границ состояния. Проверяйте не только сеть, но и визуальную стабильность, ошибки консоли и интерактивность.</p><h2>Проверяемый критерий готовности</h2><p>Диагностика готова, если для страницы есть серия повторяемых замеров, отдельные значения TTFB и передачи HTML, список ранних ресурсов с initiator и запись о главном потоке. Для выбранной гипотезы названо одно действие, а повторный прогон показывает изменение именно целевого участка. Первый экран не ухудшился, консоль не получила новую ошибку, а отрицательный путь проверен на медленной сети. Если команда может сказать только «страница стала быстрее», причина и критерий ещё не определены.</p><h2>Проверяемые источники</h2><ul><li><a href='https://www.w3.org/TR/2026/WD-navigation-timing-2-20260225/' target='_blank' rel='noopener noreferrer'>W3C Navigation Timing Level 2</a> — рабочий черновик от 25 февраля 2026 года. Определяет временные точки навигации; не сообщает пороги качества конкретного браузера и не заменяет полевой сбор.</li><li><a href='https://www.w3.org/TR/2026/CRD-resource-timing-20260420/' target='_blank' rel='noopener noreferrer'>W3C Resource Timing</a> — Candidate Recommendation Draft от 20 апреля 2026 года. Определяет записи ресурсов и их временные поля; не доказывает причинность задержки и зависит от политики доступа к данным.</li></ul>"}