{ "index": 19, "slug": "editorial-2027-06-field-performance-capstone", "title": "p95 не изменился: как найти настоящую причину медленной страницы", "excerpt": "Уменьшение bundle не гарантирует быстрый ответ. Разбираем полевой симптом, разделяем сервер, сеть и браузер, а затем принимаем решение по повторяемому p95.", "contentHtml": "

После релиза JavaScript-бандл стал меньше на 180 КБ, но p95 загрузки каталога остался около 3,1 секунды. Пользователь по-прежнему видит пустой первый экран. Цена ошибки — потратить спринт на минификацию, а затем обнаружить, что запрос к базе ждёт 1,8 секунды или браузер тратит время на главный поток. Размер файла изменился; причина задержки могла остаться прежней.

Числа в этом вступительном сценарии — иллюстрация, а не выгрузка telemetry конкретного проекта. Практический вопрос от этого не меняется: как доказать, какой участок пути тормозит страницу? Разложим один запуск на ожидание ответа, передачу документа и ресурсов, работу главного потока и отрисовку. Затем повторим измерение с теми же условиями. Результат становится инженерным доказательством, когда рядом с ним сохранены входы, сырые наблюдения, метод расчёта и критерий решения.

Что именно измеряет p95

p95 — это выбранный квантиль распределения: после сортировки наблюдений он показывает уровень, ниже которого находится примерно 95% выборки по согласованному правилу расчёта. Оставшиеся наблюдения образуют хвост. Это не обещание каждому пользователю и не среднее время. Две команды могут получить разные p95 из одних событий, если по-разному обработают пропуски, сегменты, границы окна или интерполяцию.

Маленькая выборка делает хвост хрупким. На двадцати измерениях nearest-rank p95 фактически выбирает второе значение с конца; один поздний запуск заметно меняет результат. На пяти измерениях это ещё менее устойчиво. Поэтому в отчёте указывайте число наблюдений и храните исходные значения. Для полевой метрики отдельно фиксируйте окно агрегации, сегмент пользователей и правила исключения.

Не смешивайте показатели. TTFB — согласованная командой оценка времени до начала ответа, обычно связанная с точкой responseStart; она не описывает выполнение JavaScript. LCP — момент отрисовки крупнейшего видимого элемента в рамках правил метрики; он зависит от HTML, CSS, шрифта, изображения, viewport и устройства. Размер bundle описывает объём ресурса, но не говорит, какой запрос или задача главного потока задерживает первый экран.

Минимальный протокол сравнимого замера
ПолеПримерЗачем фиксировать
Версияcommit abc123Связать результат с кодом
Сценарийоткрыть каталог и дождаться первого экранаНе менять пользовательское действие между сериями
УсловияChromium, 1280×800, cold cacheНе смешивать разные режимы
Выборка20 повторов в лабораторииПонимать устойчивость хвоста
Сырые данныеJSON со всеми значениямиПересчитать итог и увидеть выбросы
МетрикиTTFB, LCP, p95, long tasksОтделить сервер от браузера
\"Цикл
Один фактор меняется между двумя сериями, а все остальные условия возвращаются к baseline. Так результат можно связать с конкретным решением.

Механизм: задержка складывается из разных очередей

Навигация начинается с запроса документа. До первого байта браузер ждёт сеть, proxy и сервер. Сервер в это время может ждать соединение с базой, блокировку, очередь или внешний сервис. После первого байта браузер получает остальной HTML. Затем parser обнаруживает CSS, обычные script и другие зависимости: они влияют на порядок загрузки и работу главного потока. LCP появляется только после того, как браузер нашёл и отрисовал подходящий крупнейший элемент.

Для расследования полезна рабочая карта server_wait + html_transfer + blocking_resources + main_thread_work + paint. Это не универсальная формула пользовательской метрики: этапы могут перекрываться, а браузерные и серверные часы не образуют простую арифметическую сумму. Карта нужна, чтобы задать следующий вопрос. Если до первого байта стало дольше, проверяйте сервер и сеть. Если эта часть стабильна, а LCP вырос, переходите к ресурсам, layout и JavaScript.

Navigation Timing предоставляет временные точки навигации, включая начало записи и получение первого и последнего байта. Resource Timing даёт записи о ресурсах и их инициаторах. В браузере можно начать с performance.getEntriesByType('navigation')[0] и performance.getEntriesByType('resource'), но не считать наличие записи доказательством влияния на LCP. Сопоставьте запись с waterfall, HTML-тегом, initiator и фактически изменившимся элементом.

Учебный локальный замер HTTP-пути

Этот пример намеренно ограничен loopback HTTP-путём. Он не моделирует браузер, мобильную сеть, CDN, TLS, реальную базу или пользовательский состав. Сервер добавляет 40 миллисекунд перед ответом, клиент последовательно делает двадцать запросов, сохраняет сырые времена и считает медиану и p95. Это способ проверить арифметику до анализа страницы, а не доказательство production-производительности.

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});

Сохраните код как measure.mjs и запустите в Node.js 18 или новее, где доступен глобальный fetch. Обычно значения будут немного выше 40 миллисекунд: к искусственной задержке добавятся loopback, HTTP и планирование процесса. Точное число зависит от машины. Функция использует nearest-rank только для прозрачного учебного примера; в production заранее договоритесь о методе percentile и применяйте его одинаково к baseline и новой серии.

Добавьте один искусственный выброс и увидите, что p95 меняется, хотя большинство запросов осталось прежним. Это показывает чувствительность хвоста, но не доказывает регрессию страницы. Для браузерного вывода нужны отдельные запуски с Navigation Timing, Resource Timing и наблюдением LCP. Для серверной причины полезно связать request ID с trace и при необходимости передать этапы через Server-Timing.

Какие наблюдения разделяют гипотезы

От симптома к проверке и следующему действию
НаблюдениеЧто оно поддерживаетЧто проверить дальшеОграничение вывода
TTFB и LCP выросли вместедокумент начал приходить позжеserver timing, trace, очередь, база, upstreamTTFB не указывает единственную причину
TTFB стабилен, LCP выроспроблема возникла после первого байтаwaterfall, render-blocking ресурсы, long tasks, элемент LCPнужен браузерный запуск, а не только серверный лог
bundle меньше, LCP прежнийbundle не был доминирующим участкомсравнить resource entries и CPU-профильзаявление ограничено выбранным сценарием
среднее лучше, p95 хужехвост стал тяжелее или появились выбросысырые значения, размер выборки, сегменты и нагрузкасреднее не заменяет хвостовую метрику
тайминг ресурса неполныйограничение API или cross-originorigin, Timing-Allow-Origin, браузер и тип записинельзя додумывать скрытые значения

Высокий TTFB не доказывает вину базы. Он фиксирует задержку до согласованной точки ответа; причиной могут быть соединение, очередь, proxy, серверный код или upstream. Если сервер отдаёт Server-Timing, браузер может получить названия и длительности заявленных этапов, но это сигнал для сопоставления с trace и логами, а не автоматический root cause.

Ранний script тоже не равен проблеме. Он может быть небольшим и нужным для маршрутизации. Большой script может прийти после отрисовки LCP. Смотрите на фактическую блокировку главного потока, initiator и связь с выбранным элементом. Если trace не подтверждает гипотезу, вернитесь к разбиению пути, а не усиливайте оптимизацию по привычке.

Как поставить эксперимент после изменения bundle

Сначала зафиксируйте baseline: commit, URL, действие пользователя, браузер, класс CPU, viewport, сеть, состояние cache, размер данных и время запуска. Разделите cold cache и warm cache. В первом режиме часть ресурсов и соединений отсутствует локально; во втором путь уже прогрет. Смешанная серия отвечает на неясный вопрос и может скрыть регрессию.

Затем выберите один критерий решения. Например: p95 LCP не выше согласованного порога в mobile-профиле при заданном throttling и cold cache. Рядом храните TTFB, время до responseEnd, размер переданных ресурсов и число long tasks. Сам порог — продуктовая договорённость, а не число, которое автоматически следует из W3C API.

Уберите или уменьшите одного кандидата: конкретный preload, импорт или блокирующий script. Не меняйте одновременно SQL, CDN, компрессию и порядок HTML. Повторите ту же серию, сохраните все наблюдения и сравните распределения. Если LCP не изменился, это полезный результат: bundle не был причиной на выбранном сценарии. Если улучшилось только среднее, решение ещё не подтверждено.

Порядок расследования

  1. Запишите жалобу и наблюдаемый симптом: URL, действие пользователя, метрику и диапазон значений.
  2. Привяжите замер к commit и зафиксируйте браузер, viewport, CPU, сеть, cache и размер данных.
  3. Сделайте серию одинаковых запусков, сохраните каждое значение и метод расчёта percentile.
  4. Разделите путь на ожидание ответа, передачу документа, ресурсы, главный поток и отрисовку.
  5. Выберите доминирующий участок по данным и сформулируйте одну проверяемую гипотезу.
  6. Проведите минимальное обратимое изменение, не меняя остальные условия.
  7. Повторите baseline-серию и сравните p95, связанные метрики и сырые значения.
  8. Проверьте отрицательный путь: cold cache, слабый CPU, поздний upstream, cross-origin без разрешения или отсутствующую запись.
  9. Запишите, что доказано, что осталось неизвестным, какое ограничение действует и кто сможет повторить проверку.

Границы применимости и критерий готовности

Локальный HTTP-тест проверяет арифметику и порядок работы клиента, но не пользовательскую скорость. Лабораторный browser-run помогает сравнить контролируемые условия, но не покрывает все устройства, сети и состав контента. Полевой p95 зависит от числа наблюдений, агрегации, сегментов и того, как инструмент исключает или учитывает невалидные записи. Поэтому один порог нельзя без объяснения переносить между страницами.

LCP — пользовательский сигнал о крупнейшем видимом элементе, а не универсальный показатель завершения приложения. Элемент может измениться, поздняя загрузка изображения может сдвинуть момент, а одно и то же значение не раскрывает серверную причину. TTFB также не заменяет trace. Для честного вывода указывайте, какая метрика была критерием, на какой выборке и в каких условиях она получена.

Расследование закончено, когда другая команда может открыть отчёт, увидеть baseline и повторить сравнение. В отчёте есть исходные данные, метод расчёта, один доминирующий участок, проверенная гипотеза, повтор после одного изменения и отрицательный сценарий. «Стало быстрее» недостаточно; проверяемая формулировка выглядит так: «при заданных URL, браузере, viewport, сети и cache p95 LCP изменился с A до B, а TTFB и размер ресурсов изменились на C и D».

Проверяемые источники

" }