Files

2 lines
19 KiB
JSON
Raw Permalink 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 секунды», — и начинает уменьшать JavaScript. Но эти 2,4 секунды могли включать ожидание ответа сервера, передачу HTML, построение стилей и длинную задачу на главном потоке. Цена неверной гипотезы — изменить слой, который не владеет задержкой, и получить новый риск без изменения симптома.</p><p>Разбор начинается с границы. TTFB (time to first byte) показывает время до первого байта ответа, но не время готовности экрана. После первого байта браузер ещё получает тело документа, строит DOM и CSSOM, загружает ресурсы и выполняет код. Ниже — маршрут, который связывает жалобу пользователя с одним участком критического пути и позволяет проверить исправление в тех же условиях.</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 - startTime</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 - startTime</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/navigation-timing-2/' target='_blank' rel='noopener noreferrer'>W3C Navigation Timing Level 2</a> — спецификация интерфейса <code>PerformanceNavigationTiming</code> и временных точек навигации. Документ имеет статус Working Draft, поэтому описывает API, а не пороги качества конкретного сайта.</li><li><a href='https://www.w3.org/TR/resource-timing/' target='_blank' rel='noopener noreferrer'>W3C Resource Timing</a> — записи ресурсов, временные поля и тип инициатора. Доступность части данных зависит от политики браузера и условий доступа.</li></ul>"}