8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"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) => {\n setTimeout(() => response.end('ready'), 40);\n});\n\nawait new Promise((resolve) =>\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 < 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) => server.close(resolve));\n}\n\nfunction nearestRank(values, rank) {\n const sorted = [...values].sort((a, b) => 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) => 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>"
|
||
}
|