Время загрузки страницы и ресурсов

Тайминги навигации (Navigation timings) - это показатели, указывающие временные метки, в которые произошли события навигации. Тайминги ресурсов (Resource timings) - это детальные показатели по времени загрузки ресурсов. 

В этой статье мы рассмотрим как Performance Timing API, так и Performance Entry API. И хотя первый API считается устаревшим, он все ещё поддерживается всеми браузерами, он прост и о нем полезно знать. В свою очередь, Performance Entry API является более продвинутым инструментом, который позволяет не только получить более сложные данные, но и позволяет разработчику измерять другие показатели, в дополнение к данным о навигации и загрузке ресурсов.

Performance Timing API

PerformanceTiming API - это JavaScript API для измерения времени загрузки страницы. Этот API считается устаревшим, но поддерживается во всех браузерах. На текущий момент рекомендуется использовать performanceNavigationTiming API.

PerformanceTiming API предоставляет собой read only данные в виде объекта, где значениями полей являются числа, указывающие на количество миллисекунд, которые прошли к моменту срабатывания того или иного события. Как показано на изображении ниже, процесс навигации можно разбить на следующие этапы: navigationStart, unloadEventStart, unloadEventEnd, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart , connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd, domLoading, domInteractive, domContentLoadedEventStart, domContentLoadedEventEnd, domComplete, loadEventStart, и loadEventEnd.

Navigation Timing event metrics

Благодаря этим метрикам и небольшим вычислениям мы можем определить важные показатели, например время до первого байта (time to first byte), скорость загрузки страницы, поиска записи dns и даже узнать, является ли соединение безопасным.

Чтобы получить доступ к этим данным, обратитесь к следующему объекту:

let time = window.performance.timing

Мы можем использовать эти данные, чтобы понять, как быстро работает приложение:

entering window.performance.timing in the console lists all the timings in the PerformanceNavigationTiming interface

Описание показателей:

Показатель Пояснение
navigationStart

Момент, когда предыдущий документ в том же контексте (табе) запускает событие unload. Если предыдущего документа не было, значение этого показателя будет таким же, как и PerformanceTiming.fetchStart.

secureConnectionStart

Началась установка (handshake) безопасного соединения. Если безопасного соединения нет, то значение равно 0.

redirectStart Начало первого HTTP редиректа. Если никаких редиректов не было, или один из редиректов перевёл документ на другой origin, то значение равно 0.
redirectEnd

Последний HTTP редирект завершён, то есть последний байт HTTP-ответа был получен. Если никаких редиректов не было, или один из редиректов перевёл документ на другой origin, то значение равно 0.

connectStart

Запрос на открытие соединения отправлен в сеть. Если транспортный уровень модели OSI сообщает об ошибке и установка соединения запускаются заново, то возвращается время старта последней попытки соединения. Если используется постоянное соединение, то значение показателя будет таким же, как и PerformanceTiming.fetchStart.

connectEnd

Момент, когда соединение открыто для передачи данных. Если транспортный уровень модели OSI сообщает об ошибке и установка соединения запускаются заново, то возвращается время  завершения последней попытки соединения. Если используется постоянное соединение, то значение показателя будет таким же, как и PerformanceTiming.fetchStart. Соединение считается открытым, когда завершены все этапы установление безопасного соединения, например TLS Handshake или SOCKS Authentication.

domainLookupEnd

Поиск домена завершён. Если используется постоянное соединение, или используются данные, сохраненные в локальном кэше, то значение показателя будет таким же, как и PerformanceTiming.fetchStart.

domainLookupStart Начался поиск домена. Если используется постоянное соединение, или используются данные, сохраненные в локальном кэше, то значение показателя будет таким же, как и PerformanceTiming.fetchStart.
fetchStart

Браузер готов к загрузке документа с помощью HTTP-запроса. Этот этап всегда срабатывает до проверки кэша приложения.

requestStart

Браузер посылает запрос на получение документа с сервера или из кэша. Если транспортный уровень сообщает об ошибке отправки запроса, а соединение переоткрывается - этот показатель будет перезаписан данными нового запроса.

responseStart

Браузер получает первый байт ответа от сервера, кэша или локального ресурса.

responseEnd

Браузер получает последний байт ответа от сервера, кэша или локального ресурса. Если соединение закрывается раньше получения последнего байта - значение параметра указывает на момент закрытия соединения.

domLoading

Парсер HTML начинает работу. В этот момент Document.readyState изменяется на 'loading' и срабатывает событие readystatechange.

unloadEventStart

Срабатывает событие unload>, что говорит о времени, когда предыдущий документ начал выгружаться. Если предыдущего документа не было или переход к текущей странице подразумевал изменение origin (в т.ч. из-за редиректов), значение параметра равно 0.

unloadEventEnd

Обработчик события  unload завершил свою работу. Если предыдущего документа не было или переход к текущей странице подразумевал изменение origin (в т.ч. из-за редиректов), значение параметра равно 0.

domInteractive

HTML парсер завершил работу над основным документом. В этот момент Document.readyState изменяется на 'interactive' и срабатывает событие readystatechange

domContentLoadedEventStart

Момент сразу перед тем, как парсер запускает событие DOMContentLoaded. Это событие запускается после того, как все скрипты, которые должны исполниться сразу после парсинга, выполнены.

domContentLoadedEventEnd

Момент сразу после исполнения всех скриптов, которые должны были исполниться.

domComplete

Парсер HTML завершил работу над основным документом. В этот момент Document.readyState изменяется на 'complete' и срабатывает событие readystatechange.

loadEventStart

Событие load было отправлено текущему документу. Если это событие на момент измерения не было отправлено документу, значение параметра равно 0.

loadEventEnd

Обработка события load завершена, то есть загрузка завершена. Если это событие ещё не произошло или не было послано документу, значение параметра равно 0.

Вычисление таймингов

Мы можем использовать все эти значения, чтобы вычислить, сколько времени потребовалось на тот или иной этап:

let 
    dns  = time.domainLookupEnd - time.domainLookupStart,
    tcp  = time.connectEnd - time.connectStart, 
    ssl != time.secureConnectionStart,

Время до первого байта

Время до первого байта (Time to First Byte) - это время между navigationStart и responseStart (момент, когда получен первый байт от сервера / кэша). Доступно в performanceTiming API

let ttfb = time.responseStart - time.navigationStart;

Время загрузки страницы

Время загрузки страницы (Page load time) - это время между navigationStart и моментом, когда событие load отправлено текущего документу. Доступно только в performanceTiming API

let pageloadtime = time.loadEventStart - time.navigationStart;

Время поиска записи DNS

Время поиска записи DNS (DNS lookup) - это время между domainLookupStart и domainLookupEnd. Оба эти параметра доступны как в performanceTiming, так и в performanceNavigationTiming.

let dns  = time.domainLookupEnd - time.domainLookupStart;

TCP

Время установки соединения TCP - это время между началом и окончанием попытки соединения:

tcp  = time.connectEnd - time.connectStart;

Установка безопасного подключения (SSL negotiation)

secureConnectionStart будет равен undefined, если SSL не доступен, 0 если https не используется или если временная метка доступна и используется. Другими словами, если безопасное соединение было использовано, то значение secureConnectionStart будет правдиво (truthy), а время между secureConnectionStart и requestStart будет больше 0.

ssl = time.requestStart - time.secureConnectionStart;

Performance Entry API

Основные показатели производительности, рассмотренные выше, считаются устаревшими, но полностью поддерживаются современными браузерами. Взамен предлагается использовать Performance Entry API, который предоставляет инструмент для пометок и измерений времени одновременно с событиями navigation и загрузкой resource. Вы также можете создавать свои маркеры:

performance.getEntriesByType('navigation').forEach((navigation) => {
  console.dir(navigation);
});

performance.getEntriesByType('resource').forEach((resource) => {
  console.dir(resource);
});

performance.getEntriesByType('mark').forEach((mark) => {
  console.dir(mark);
});

performance.getEntriesByType("measure").forEach((measure) => {
  console.dir(measure);
});

performance.getEntriesByType('paint').forEach((paint) => { 
  console.dir(paint); 
});

performance.getEntriesByType('frame').forEach((frame) => {
  console.dir(frame);
});

В некоторых браузерах вы можете использовать performance.getEntriesByType('paint'), чтобы запросить измерения для  first-paint и first-contentful-paint. Мы используем  performance.getEntriesByType('navigation') и  performance.getEntriesByType('resource') для запроса данных по навигации и загрузки ресурсов, соответственно.

Когда пользователь запрашивает веб-приложение, браузер должен получить некоторые мета-данные, чтобы начать загрузку. Для этого пользовательский агент проходит серию шагов, такие как поиск записи DNS (DNS lookup), TCP рукопожатие TCP handshake, и установку безопасного соединения (SSL negotiation). Как только браузер установил соединение, происходит первый полезный запрос данных на сервера. Как только начинают поступать данные от сервера, браузер начинает парсить полученные данные, строит DOM, CSSOM, создает деревья рендера (render trees), чтобы в конце концов отрендерить страницу. В тот момент, когда браузер перестает парсить входящие данные, документ переходит в интерактивную стадию. Если в документе существуют отложенные к загрузке ресурсы (deferred scripts), которые должны быть обработаны, браузер парсит их. После этого запускается событие DOMContentLoaded, после которого готовность страницы завершена. Теперь документ может обрабатывать пост-загрузочные задачи. После этого документ маркируется, как полностью загруженный.

let navigationTimings = performance.getEntriesByType('navigation');

Метод performance.getEntriesByType('navigation') возвращает массив PerformanceEntry, в котором содержатся объекты Navigation Timing.

The results of when performance.getEntriesByType('navigation'); is entered into the console for this document

Из этих данных можно многое извлечь. На изображении выше вы видите, что помимо самих таймингов, данные содержат имя документа и некоторую другую полезную информацию.

let timing = performance.getEntriesByType('navigation')[0];

Протокол

Мы можем проверить протокол, который используется дл получения ресурсов:

let protocol = timing.nextHopProtocol

В текущем случае в ответ будет h2 для http/2.

Сжатие

Чтобы узнать, как эффективно сжимаются данные при передаче, мы можем разделить  transferSize на decodedBodySize, а затем вычесть результат из 100%. Для текущей страницы сжатие составляет до 74%.

let compressionSavings = 1 - (timing.transferSize / timing.decodedBodySize)

Мы могли бы использовать

let compressionSavings = 1 - (timing.encodedBodySize / timing.decodedBodySize)

но transfersize так же включает в себя байты заголовков.

Для сравнение, мы можем посмотреть на вкладку Network, где увидим, что было передано 22.04KB для файла, который в разархивированном виде занимает 87.24KB.

View of the bytes transferred and the size via the network tab

Если мы проверим вычисления, то результат получится схожим: 1 - (22.04 / 87.24) = 0.747. Тайминги навигации позволяют нам получить такие данные программно.

Обратите внимание, что это данные для одного единственно документа, а не для всех ресурсов вместе взятых. В то же время, длительность загрузки, события-обработчики и тайминиги построения DOM / CSSOM влияют на продолжительность загрузки всего приложения, не только одного конкретного ресурса. Клиентские приложения, выполняющиеся в браузере, могут выглядеть быстрее, если данные объемом 300КБ вы передаете сжатыми до 100КБ, но это все не значит, что JavaScript, CSS или другие медиа-ресурсы не раздувают приложение и не делают его медленее. Проверка уровня сжатия - это очень важно, но не менее важно проверять длительность парсинга ресурсов и время между тем, как завершен DOMContentLoaded и DOM готов к работе. Может случиться так, что время парсинга скриптов и обработка скриптами результатов в основном потоке (main thread) приведет к зависанию интерфейса.

Время запроса

API не предоставляет все измерения, которые разработчик хочет получить. Например, как долго продлилось выполнение запроса? Отдельного поля в объекте данных нет. Однако, мы можем использовать измерения, чтобы вычислить то, что нам нужно.

Чтобы определить время ответа, вычтите время старта запроса из времени старта получения ответа. Запрос стартует ровно в тот момент, когда клиент запрашивает ресурс с сервера (или из кэша). Ответ начинается ровно в тот момент, когда клиент получает первый байт.

request = timing.responseStart - timing.requestStart

Длительность события загрузки

load = timing.loadEventEnd - timing.loadEventStart 

DOMContentLoaded event

Длительность события DOMContentLoaded определяется разностью моментов, когда клиент запускает событие DOMContentLoaded и когда это событие завершено. Старайтесь держать эту величину меньше 50ms - тогда ваш интерфейс будет отзывчивым.

DOMContentLoaded = timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart

Длительность (Duration)

В объекте данных есть поле Длительность (Duration). Длительность - это разница между PerformanceNavigationTiming.loadEventEnd и PerformanceEntry.startTime properties.

Интерфейс PerformanceNavigationTiming, кроме того, дает информацию о том, какой тип навигации вы измеряете, возвращая navigate, reload, back_forward или prerender.

Resource

В то время, как тайминги навигации измеряют произодительность загрузки и парсинга основного файла HTML, этот файл служит лишь точкой входа для загрузки других ресурсов. Поэтому нам так же важно знать, как быстро загружаются дополнительные ресурсы. Для измерения этих данных нужно использовать Resource Timing. Большая часть измерений в этом объекте похожи: здесь и поиск домена в DNS, и TCP установка соединения и т.д.

Graphic of Resource Timing timestamps

Для того, чтобы получить эти данные, выполните команду:

performance.getEntriesByType("resource")

См. также