Files

8 lines
22 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": 19,
"slug": "editorial-2027-06-field-performance-capstone",
"title": "p95 не изменился: как найти настоящую причину медленной страницы",
"excerpt": "Уменьшение bundle не гарантирует быстрый ответ. Разбираем полевой симптом, разделяем сервер, сеть и браузер, а затем принимаем решение по повторяемому p95.",
"contentHtml": "<p>После релиза JavaScript-бандл стал меньше на 180 КБ, но p95 загрузки каталога остался около 3,1 секунды. Пользователь по-прежнему видит пустой первый экран. Цена ошибки — потратить спринт на минификацию, а затем обнаружить, что запрос к базе ждёт 1,8 секунды или браузер тратит время на главный поток. Размер файла изменился; причина задержки могла остаться прежней.</p><p>Числа в этом вступительном сценарии — иллюстрация, а не выгрузка telemetry конкретного проекта. Практический вопрос от этого не меняется: как доказать, какой участок пути тормозит страницу? Разложим один запуск на ожидание ответа, передачу документа и ресурсов, работу главного потока и отрисовку. Затем повторим измерение с теми же условиями. Результат становится инженерным доказательством, когда рядом с ним сохранены входы, сырые наблюдения, метод расчёта и критерий решения.</p><h2>Что именно измеряет p95</h2><p>p95 — это выбранный квантиль распределения: после сортировки наблюдений он показывает уровень, ниже которого находится примерно 95% выборки по согласованному правилу расчёта. Оставшиеся наблюдения образуют хвост. Это не обещание каждому пользователю и не среднее время. Две команды могут получить разные p95 из одних событий, если по-разному обработают пропуски, сегменты, границы окна или интерполяцию.</p><p>Маленькая выборка делает хвост хрупким. На двадцати измерениях nearest-rank p95 фактически выбирает второе значение с конца; один поздний запуск заметно меняет результат. На пяти измерениях это ещё менее устойчиво. Поэтому в отчёте указывайте число наблюдений и храните исходные значения. Для полевой метрики отдельно фиксируйте окно агрегации, сегмент пользователей и правила исключения.</p><p>Не смешивайте показатели. TTFB — согласованная командой оценка времени до начала ответа, обычно связанная с точкой <code>responseStart</code>; она не описывает выполнение JavaScript. LCP — момент отрисовки крупнейшего видимого элемента в рамках правил метрики; он зависит от HTML, CSS, шрифта, изображения, viewport и устройства. Размер bundle описывает объём ресурса, но не говорит, какой запрос или задача главного потока задерживает первый экран.</p><table><caption>Минимальный протокол сравнимого замера</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Пример</th><th scope=\"col\">Зачем фиксировать</th></tr></thead><tbody><tr><td>Версия</td><td>commit abc123</td><td>Связать результат с кодом</td></tr><tr><td>Сценарий</td><td>открыть каталог и дождаться первого экрана</td><td>Не менять пользовательское действие между сериями</td></tr><tr><td>Условия</td><td>Chromium, 1280×800, cold cache</td><td>Не смешивать разные режимы</td></tr><tr><td>Выборка</td><td>20 повторов в лаборатории</td><td>Понимать устойчивость хвоста</td></tr><tr><td>Сырые данные</td><td>JSON со всеми значениями</td><td>Пересчитать итог и увидеть выбросы</td></tr><tr><td>Метрики</td><td>TTFB, LCP, p95, long tasks</td><td>Отделить сервер от браузера</td></tr></tbody></table><figure><img src=\"/assets/editorial/2027/performance-capstone-2027-handoff-loop.svg\" alt=\"Цикл проверки: зафиксировать условия, сделать серию запусков, найти доминирующий участок, изменить один фактор и повторить замер\" loading=\"lazy\" /><figcaption>Один фактор меняется между двумя сериями, а все остальные условия возвращаются к baseline. Так результат можно связать с конкретным решением.</figcaption></figure><h2>Механизм: задержка складывается из разных очередей</h2><p>Навигация начинается с запроса документа. До первого байта браузер ждёт сеть, proxy и сервер. Сервер в это время может ждать соединение с базой, блокировку, очередь или внешний сервис. После первого байта браузер получает остальной HTML. Затем parser обнаруживает CSS, обычные script и другие зависимости: они влияют на порядок загрузки и работу главного потока. LCP появляется только после того, как браузер нашёл и отрисовал подходящий крупнейший элемент.</p><p>Для расследования полезна рабочая карта <code>server_wait + html_transfer + blocking_resources + main_thread_work + paint</code>. Это не универсальная формула пользовательской метрики: этапы могут перекрываться, а браузерные и серверные часы не образуют простую арифметическую сумму. Карта нужна, чтобы задать следующий вопрос. Если до первого байта стало дольше, проверяйте сервер и сеть. Если эта часть стабильна, а LCP вырос, переходите к ресурсам, layout и JavaScript.</p><p>Navigation Timing предоставляет временные точки навигации, включая начало записи и получение первого и последнего байта. Resource Timing даёт записи о ресурсах и их инициаторах. В браузере можно начать с <code>performance.getEntriesByType('navigation')[0]</code> и <code>performance.getEntriesByType('resource')</code>, но не считать наличие записи доказательством влияния на LCP. Сопоставьте запись с waterfall, HTML-тегом, initiator и фактически изменившимся элементом.</p><h2>Учебный локальный замер HTTP-пути</h2><p>Этот пример намеренно ограничен loopback HTTP-путём. Он не моделирует браузер, мобильную сеть, CDN, TLS, реальную базу или пользовательский состав. Сервер добавляет 40 миллисекунд перед ответом, клиент последовательно делает двадцать запросов, сохраняет сырые времена и считает медиану и p95. Это способ проверить арифметику до анализа страницы, а не доказательство production-производительности.</p><pre><code>import { performance } from 'node:perf_hooks';\nimport { createServer } from 'node:http';\n\nconst server = createServer((_request, response) =&gt; {\n setTimeout(() =&gt; response.end('ready'), 40);\n});\n\nawait new Promise((resolve) =&gt;\n server.listen({ host: '127.0.0.1', port: 0 }, resolve)\n);\n\nconst { port } = server.address();\nconst samples = [];\n\ntry {\n for (let attempt = 0; attempt &lt; 20; attempt += 1) {\n const started = performance.now();\n const response = await fetch('http://127.0.0.1:' + port);\n await response.text();\n samples.push(performance.now() - started);\n }\n} finally {\n await new Promise((resolve) =&gt; server.close(resolve));\n}\n\nfunction nearestRank(values, rank) {\n const sorted = [...values].sort((a, b) =&gt; a - b);\n const index = Math.max(0, Math.ceil(sorted.length * rank) - 1);\n return sorted[index];\n}\n\nconsole.log({\n count: samples.length,\n medianMs: nearestRank(samples, 0.5).toFixed(1),\n p95Ms: nearestRank(samples, 0.95).toFixed(1),\n samplesMs: samples.map((value) =&gt; value.toFixed(1)),\n});</code></pre><p>Сохраните код как <code>measure.mjs</code> и запустите в Node.js 18 или новее, где доступен глобальный <code>fetch</code>. Обычно значения будут немного выше 40 миллисекунд: к искусственной задержке добавятся loopback, HTTP и планирование процесса. Точное число зависит от машины. Функция использует nearest-rank только для прозрачного учебного примера; в production заранее договоритесь о методе percentile и применяйте его одинаково к baseline и новой серии.</p><p>Добавьте один искусственный выброс и увидите, что p95 меняется, хотя большинство запросов осталось прежним. Это показывает чувствительность хвоста, но не доказывает регрессию страницы. Для браузерного вывода нужны отдельные запуски с Navigation Timing, Resource Timing и наблюдением LCP. Для серверной причины полезно связать request ID с trace и при необходимости передать этапы через <code>Server-Timing</code>.</p><h2>Какие наблюдения разделяют гипотезы</h2><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 и LCP выросли вместе</td><td>документ начал приходить позже</td><td>server timing, trace, очередь, база, upstream</td><td>TTFB не указывает единственную причину</td></tr><tr><td>TTFB стабилен, LCP вырос</td><td>проблема возникла после первого байта</td><td>waterfall, render-blocking ресурсы, long tasks, элемент LCP</td><td>нужен браузерный запуск, а не только серверный лог</td></tr><tr><td>bundle меньше, LCP прежний</td><td>bundle не был доминирующим участком</td><td>сравнить resource entries и CPU-профиль</td><td>заявление ограничено выбранным сценарием</td></tr><tr><td>среднее лучше, p95 хуже</td><td>хвост стал тяжелее или появились выбросы</td><td>сырые значения, размер выборки, сегменты и нагрузка</td><td>среднее не заменяет хвостовую метрику</td></tr><tr><td>тайминг ресурса неполный</td><td>ограничение API или cross-origin</td><td>origin, <code>Timing-Allow-Origin</code>, браузер и тип записи</td><td>нельзя додумывать скрытые значения</td></tr></tbody></table><p>Высокий TTFB не доказывает вину базы. Он фиксирует задержку до согласованной точки ответа; причиной могут быть соединение, очередь, proxy, серверный код или upstream. Если сервер отдаёт <code>Server-Timing</code>, браузер может получить названия и длительности заявленных этапов, но это сигнал для сопоставления с trace и логами, а не автоматический root cause.</p><p>Ранний script тоже не равен проблеме. Он может быть небольшим и нужным для маршрутизации. Большой script может прийти после отрисовки LCP. Смотрите на фактическую блокировку главного потока, initiator и связь с выбранным элементом. Если trace не подтверждает гипотезу, вернитесь к разбиению пути, а не усиливайте оптимизацию по привычке.</p><h2>Как поставить эксперимент после изменения bundle</h2><p>Сначала зафиксируйте baseline: commit, URL, действие пользователя, браузер, класс CPU, viewport, сеть, состояние cache, размер данных и время запуска. Разделите cold cache и warm cache. В первом режиме часть ресурсов и соединений отсутствует локально; во втором путь уже прогрет. Смешанная серия отвечает на неясный вопрос и может скрыть регрессию.</p><p>Затем выберите один критерий решения. Например: p95 LCP не выше согласованного порога в mobile-профиле при заданном throttling и cold cache. Рядом храните TTFB, время до <code>responseEnd</code>, размер переданных ресурсов и число long tasks. Сам порог — продуктовая договорённость, а не число, которое автоматически следует из W3C API.</p><p>Уберите или уменьшите одного кандидата: конкретный preload, импорт или блокирующий script. Не меняйте одновременно SQL, CDN, компрессию и порядок HTML. Повторите ту же серию, сохраните все наблюдения и сравните распределения. Если LCP не изменился, это полезный результат: bundle не был причиной на выбранном сценарии. Если улучшилось только среднее, решение ещё не подтверждено.</p><h2>Порядок расследования</h2><ol><li>Запишите жалобу и наблюдаемый симптом: URL, действие пользователя, метрику и диапазон значений.</li><li>Привяжите замер к commit и зафиксируйте браузер, viewport, CPU, сеть, cache и размер данных.</li><li>Сделайте серию одинаковых запусков, сохраните каждое значение и метод расчёта percentile.</li><li>Разделите путь на ожидание ответа, передачу документа, ресурсы, главный поток и отрисовку.</li><li>Выберите доминирующий участок по данным и сформулируйте одну проверяемую гипотезу.</li><li>Проведите минимальное обратимое изменение, не меняя остальные условия.</li><li>Повторите baseline-серию и сравните p95, связанные метрики и сырые значения.</li><li>Проверьте отрицательный путь: cold cache, слабый CPU, поздний upstream, cross-origin без разрешения или отсутствующую запись.</li><li>Запишите, что доказано, что осталось неизвестным, какое ограничение действует и кто сможет повторить проверку.</li></ol><h2>Границы применимости и критерий готовности</h2><p>Локальный HTTP-тест проверяет арифметику и порядок работы клиента, но не пользовательскую скорость. Лабораторный browser-run помогает сравнить контролируемые условия, но не покрывает все устройства, сети и состав контента. Полевой p95 зависит от числа наблюдений, агрегации, сегментов и того, как инструмент исключает или учитывает невалидные записи. Поэтому один порог нельзя без объяснения переносить между страницами.</p><p>LCP — пользовательский сигнал о крупнейшем видимом элементе, а не универсальный показатель завершения приложения. Элемент может измениться, поздняя загрузка изображения может сдвинуть момент, а одно и то же значение не раскрывает серверную причину. TTFB также не заменяет trace. Для честного вывода указывайте, какая метрика была критерием, на какой выборке и в каких условиях она получена.</p><p>Расследование закончено, когда другая команда может открыть отчёт, увидеть baseline и повторить сравнение. В отчёте есть исходные данные, метод расчёта, один доминирующий участок, проверенная гипотеза, повтор после одного изменения и отрицательный сценарий. «Стало быстрее» недостаточно; проверяемая формулировка выглядит так: «при заданных URL, браузере, viewport, сети и cache p95 LCP изменился с A до B, а TTFB и размер ресурсов изменились на C и D».</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>responseStart</code> и <code>responseEnd</code>. Это API, а не готовый порог качества; измерительный контекст зависит от браузера.</li><li><a href=\"https://www.w3.org/TR/resource-timing/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Resource Timing</a> — интерфейс таймингов HTTP-ресурсов и ограничения для cross-origin записей. Для части полей может потребоваться заголовок <code>Timing-Allow-Origin</code>; документ может обновляться.</li><li><a href=\"https://www.w3.org/TR/server-timing/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Server Timing</a> — контракт передачи серверных этапов в браузер через HTTP-заголовок. Он сообщает заявленные сервером этапы и не доказывает причину без проверки на сервере.</li><li><a href=\"https://web.dev/articles/lcp\" target=\"_blank\" rel=\"noopener noreferrer\">web.dev: Largest Contentful Paint</a> — официальное объяснение LCP как сигнала загрузки основного содержимого и его ограничений. Для причин анализа нужны дополнительные лабораторные или полевые данные.</li><li><a href=\"https://web.dev/articles/lab-and-field-data-differences\" target=\"_blank\" rel=\"noopener noreferrer\">web.dev: Why lab and field data can be different</a> — официальное описание различий между контролируемыми лабораторными замерами и реальными пользовательскими наблюдениями.</li></ul>"
}