Фраза «после оптимизации стало быстрее» не даёт проверить ни исходное условие, ни размер выигрыша. Цена ошибки — принять удачный прогрев кэша за эффект изменения и откатить полезную работу, когда другой браузер покажет обратный результат. Измерение начинается с описания входа, а не с красивого числа.
Соберём маленький локальный стенд: сервер отдаёт страницу с фиксированной задержкой, клиент делает серию запросов и считает медиану и p95. Такой тест не заменяет браузерный прогон, но показывает дисциплину измерения: одинаковый URL, количество повторов, единица времени, выбросы и границы вывода.
Число имеет смысл только вместе с условиями
Для сравнения нужны версия сборки, браузер, viewport, режим кэша, сеть, размер данных и порядок действий. Если поменялось сразу пять условий, разность не принадлежит одному изменению. В отчёте сначала фиксируйте входные данные, затем распределение наблюдений и только потом пишите вывод.
| Параметр | Пример значения | Зачем |
|---|---|---|
| URL и commit | /catalog, abc123 | что именно измеряли |
| Браузер и viewport | Chromium, 1280×800 | какой клиент выполнял код |
| Кэш | cold или warm | что уже было в storage |
| Повторы | 20 | насколько устойчиво число |
| Метрика | TTFB, p95 | какой участок сравниваем |
Локальный p95 на серии запросов
Вход кода — число повторов и локальный endpoint. Сервер отвечает после одной и той же задержки, а клиент измеряет round trip. Ожидаемый результат — отсортированный список и p95, близкий к заданной задержке плюс накладные расходы Node. Это рабочий замер локального HTTP-пути, а не оценка интерфейса.
import { performance } from 'node:perf_hooks';
import { createServer as createMeasureServer } from 'node:http';
const server = createMeasureServer((request, response) => {
setTimeout(() => response.end('ready'), 40);
});
function percentile(values, rank) {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * rank) - 1)];
}
server.listen({ host: '127.0.0.1', port: 0 }, async function measure() {
const address = server.address();
const port = address.port;
const samples = [];
for (let index = 0; index < 20; index += 1) {
const started = performance.now();
await (await fetch('http://127.0.0.1:' + port)).text();
samples.push(performance.now() - started);
}
console.log({ count: samples.length, median: percentile(samples, 0.5).toFixed(1), p95: percentile(samples, 0.95).toFixed(1) });
server.close();
});В результате count равен 20, медиана и p95 находятся около 40 миллисекунд. На загруженной машине числа выше, но их разброс остаётся видимым. Функция percentile здесь намеренно короткая: на малой выборке p95 — один элемент, поэтому отчёт должен сохранять размер серии и сами условия, а не только округлённый итог.
Cold и warm cache нельзя смешивать в одной выборке. Если нужен общий отчёт, маркируйте режим каждой строки и сравнивайте одинаковые поднаборы. Иначе p95 может вырасти только потому, что несколько первых запросов скачали ресурсы.
Среднее скрывает хвост
Среднее удобно для оценки общей стоимости, но пользователь попадает в хвост распределения. p95 означает, что 95 процентов измерений не превышают значение, а пять процентов превышают. Это не «медленность каждого запроса». При малом числе повторов один выброс сильно меняет p95, поэтому сравнивайте одинаковые объёмы выборки.
Для страницы полезно хранить несколько метрик отдельно: TTFB, время до DOMContentLoaded, LCP, long tasks и размер ответа. Нельзя складывать их в один балл без явного решения. LCP зависит от содержимого и браузера, а TTFB — от серверной и сетевой части. Изменение, которое улучшило один участок, может ухудшить другой.
Как не перепутать корреляцию с причиной
| Сигнал | Можно сказать | Нужна дополнительная проверка |
|---|---|---|
| TTFB выше | ответ начал приходить позже | backend, сеть и proxy |
| p95 вырос | хвост стал тяжелее в этих условиях | нагрузка и размер выборки |
| script появился раньше | порядок ресурсов изменился | влияние на render и DOM |
| LCP изменился | визуальный критерий изменился | какой элемент стал LCP |
Слово «корреляция» полезно только вместе с ограничением. Если TTFB и LCP выросли одновременно, это повод проверить сервер, но не доказательство, что сервер — единственная причина. Свяжите метрики по одному запуску, сохраните network log и посмотрите, какой элемент стал самым поздним крупным контентом.
Порядок замера
- Зафиксировать URL, код сборки, браузер, viewport, сеть и режим кэша.
- Прогреть или очистить кэш одинаковым способом перед каждым набором.
- Сделать одинаковое число повторов и сохранить сырые значения времени.
- Посчитать median, p95 и диапазон, указав единицу измерения.
- Изменить один фактор и повторить тот же набор.
- Записать, какая метрика улучшилась, какая ухудшилась и какие вопросы остались открытыми.
Ограничения и следующий шаг
Локальный HTTP-сервер не моделирует мобильную сеть, CPU браузера, кэш CDN и работу layout. Маленькая серия не годится для долгих выводов. W3C описывает интерфейсы измерения, но не устанавливает ваш порог приемки. Порог должен учитывать страницу, устройство и стоимость задержки.
Следующий шаг — добавить к локальной серии один контролируемый browser-run и сохранить raw JSON вместе с условиями. Проверяйте распределение, а не только среднее. Если результат меняется после очистки кэша или при другом viewport, сначала уточните входные данные, затем выбирайте оптимизацию.
Проверяемые источники
- W3C Navigation Timing Level 2 — Working Draft, 25 February 2026. Определяет навигационные временные метки и связь между этапами загрузки документа. Граница применимости: Рабочий черновик стандарта не сообщает значения метрик вашего браузера и не заменяет полевой сбор.
- W3C Performance Timeline — Candidate Recommendation Draft, 21 May 2025. Определяет PerformanceEntry, getEntries и PerformanceObserver как основу чтения метрик. Граница применимости: Спецификация не задаёт пороги качества и не гарантирует одинаковый набор entry во всех движках.
- W3C Resource Timing — Candidate Recommendation Draft, 20 April 2026. Определяет записи ресурсов, размеры и render-blocking status для сетевого слоя страницы. Граница применимости: Не описывает серверную архитектуру, пользовательский опыт целиком или причинность конкретной задержки.