JavaScript

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

Существует множество причин, по которым ваш сайт загружается медленно, и знание о том, где он виснет, — изначальный и первый шаг на пути исправления проблемы. Вы можете воспользоваться консолью разработчика, проверив на вкладке Network, сколько времени уходит на загрузку ваших ресурсов и сколько времени требуется после этого для отображения первой страницы. Это уже может пролить на вещи кое-какой свет, но по-прежнему дает информацию исключительно о том, как все работает у людей с тем же оборудованием, что и у вас, а также сидящих в том же офисе. Так что этого мало.

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

Синтетический и реальный мониторинг

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

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

Моделирование — это замечательно, особенно когда у вас нет никаких реальных данных, но невозможно смоделировать каждую конкретную ситуацию. После выхода вашего приложения в широкий мир полезной практикой будет отслеживание его поведения с настоящими пользователями. Это называется мониторингом реальных пользователей (Real User Monitoring) или RUM.

Интерфейс Performance

Для мониторинга нашего приложения в естественных условиях в JavaSсript есть интерфейс под названием Performance, предоставляющий информацию относительно производительности. Этот интерфейс расширяется четырьмя следующими API.

Navigation Timing API предоставляет данные о производительности, связанные с загрузкой документа. Resource Time API предоставляет эти данные относительно загрузки ресурсов приложения (изображений, скриптов и так далее). User Timing API позволяет разработчику создавать свои собственные метрики, помещая некоторые “отметки” и “измерения” в код приложения. И, наконец, Performance Timeline API комбинирует все эти метрики на временной шкале и предоставляет способы их извлечения.

(Некоторые фичи, которые мы собираемся обсудить здесь, все еще экспериментальны. Они реализованы в Firefox и Google Chrome, но, например, не в Safari).

Временная шкала и записи производительности

Вы можете получить доступ к временной шкале производительности и ее записям из объекта window. Заходим на любой сайт и в консоли запускаем:

performance.getEntries();

Для google.com эта команда возвращает примерно 24 записи:

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

Как можете видеть, в нашем списке есть разные типы записей, соответствующие различным имеющимся у нас интерфейсам. Эти различные типы соответствуют API, рассмотренным нами выше. Вы можете запросить только записи какого-то определенного типа с помощью метода getEntriesByType.

performance.getEntriesByType('navigation');

Запись Performance Navigation Timing

Запись Navigation Timing дает вам информацию о производительности при начальной загрузке вашего сайта. Если вы хотите видеть только эти метрики, вы можете запросить только навигационные записи:

performance.getEntriesByType('navigation');

Вот мои результаты для google.com:

Все эти параметры дают информацию о времени, которое требуется на выполнение каждой задачи, необходимой для загрузки вашей страницы. Отсчет начинается с того момента, когда пользователь инициировал загрузку, например, кликнул на ссылку или нажал Enter после ввода домена в строке URL. Атрибуты, которые вы видите, соответствуют следующим этапам:

Источник: W3.org

Сначала может потребоваться некоторое время для выгрузки предыдущей страницы, на которой вы находились, что соответствует событиям unloadEventStart и unloadEventEnd. Затем вам может потребоваться перенаправить пользователя на новый домен. Это события redirectStart и redirectEnd. В нашем случае не возникло необходимости ни в выгрузке, ни в перенаправлении. Мы начинаем с фактического извлечения приложения, вызывая событие fetchStart. Мы проверяем, что есть у нас в кэше, но чтобы загрузить что-то с сервера, нам нужно разрешить IP из доменного имени. Это соответствует событиям domainLookupStart и domainLookupEnd. В нашем примере этот процесс начался после 4 мс и закончился почти на 22 мс.

Затем мы начинаем устанавливать соединение с сервером с первого TCP-рукопожатия (connectStart), продолжая TLS-рукопожатиями (secureConnectionStart). Когда соединение установлено (connectEnd), мы можем начать запрашивать HTML-документ (requestStart). В нашем примере вы можете видеть, что уже прошло 90 мс, прежде чем браузер запросил страницу. Запрос должен поступить на сервер, быть обработан им, а ответ должен вернуться к нам. Когда мы получаем первый байт ответа, запускается событие responseStart. Ответ обычно разбивается на несколько TCP-пакетов, на получение которых потребуется некоторое время. Когда, наконец, у нас оказывается последний байт, то выдается событие responseEnd.

И это еще не все. Как только мы получили полный HTML-документ, браузер должен проанализировать DOM. Как только это будет сделано (событие domInteractive), нам нужно загрузить таблицы стилей и построить CSSOM. Только тогда (domContentLoadedEventStart) мы можем построить дерево рендеринга (domComplete). Наконец страница может загрузиться, и запускаются loadEventStart и loadEventEnd.

В браузерах, где еще не поддерживается интерфейс второго уровня, таких как Safari, вы можете использовать следующую команду, чтобы получить доступ почти к той же информации:

performance.timing

Запись Performance Paint Timing

Записи отрисовки помогают вам понять, когда пользователь видит что-то в первый раз.

performance.getEntriesByType('paint');

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

Записи Performance Resource Timing

Временная шкала навигации предоставляет вам метрики относительно начальной загрузки DOM. HTML-страницы обычно не поступают в одиночку и сопровождаются набором таблиц стилей и скриптов. Вы можете проверить время их загрузки с помощью записей синхронизации ресурсов.

performance.getEntriesByType('resource');

Вы можете видеть изображения и скрипты, а также “xmlhttprequests”. Даже после первоначальной загрузки и рендеринга DOM ресурсы, вероятно, будут запрашиваться с помощью JavaSсript. Относящиеся к этому метрики также будут добавлены в буфер записей при их появлении.

Записи ресурсов содержат ту же информацию, что и общая временная шкала: поиск домена, начало и конец соединения, начало и конец запроса и т.д.

Метки и измерения производительности

Возможно, вы захотите создать свои собственные метрики для проверки производительности вашего кода. Для этого можно использовать API пользовательских ресурсов (User Resource API). Он позволяет создавать метки и измерения в вашем коде.

Метка — это нечто вроде точки останова. Вы устанавливаете ее в любом месте вашего кода, и она появляется в списке записей с указанием точного времени события (от момента начала отсчета, когда пользователь инициировал переход на вашу страницу).

Если, например, вы хотите знать, когда действительно времязатратная задача будет выполнена, вы можете добавить после нее метку:

reallyLongSynchronousFunction();
performance.mark('reallyLongSynchronousFunctionDone');

Если после загрузки страницы и выполнения функции вы запросите записи меток интерфейса производительности, то увидите созданные вами метки с указанием времени:

performance.getEntriesByType('mark');

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

const startMarkName = 'reallyLongSynchronousFunctionStart';
const endMarkName = 'reallyLongSynchronousFunctionEnd';performance.mark(startMarkName);
reallyLongSynchronousFunction();
performance.mark(endMarkName);performance.measure(
  'reallyLongSynchronousFunction',
  startMarkName,
  endMarkName,
);

Вызывая записи измерений через API производительности, вы увидите продолжительность вашей действительно длительной задачи:

performance.getEntriesByType('measure');

Сбор и анализ данных

Все эти данные доступны в пользовательском браузере. Чтобы извлечь из них какую-либо полезную информацию, нам нужно собрать их у всех наших пользователей и сохранить там, где мы сможем их обработать. Мы должны отправить их через POST на наш бэкэнд, но есть два вопроса: когда и как.

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

Вы можете использовать одного или нескольких наблюдателей, чтобы получать уведомления, когда в список добавляется новая запись.

const observe_all = new PerformanceObserver((list, obs) => {  
   const entries = list.getEntries(); 
   // send them
}

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

Другой способ — просто дождаться события выгрузки (unload), чтобы собрать и отправить эти данные через POST. Вам не захочется блокировать пользователя на время отправки, поэтому вы должны использовать sendBeacon.

window.addEventListener('unload', () => {
  const data = new FormData();
  data.append('entries', JSON.stringify(performance.getEntries()));   navigator.sendBeacon('/performance-entries', data);
});

Оба метода имеют свои плюсы и минусы, и выбор зависит от вас и от того, что именно вы хотите сделать.

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

Но какие метрики действительно важны и где именно вы можете повысить производительность вашего приложения? На этот вопрос нет однозначного ответа, но вот несколько советов, которыми вы можете воспользоваться:

  • Посмотрите, нет ли у вас перенаправления в самом начале: его следует избегать, так как оно вызывает несколько круговых задержек, но не приносит пользы пользователю.
  • Если вы теряете много времени между connectStart и connectEnd, проверьте, не получится ли у вас оптимизировать свой сервер для рукопожатий TCP и TLS. (Чтобы узнать об этом больше, рекомендую книгу “High Performance Browser Networking” Ильи Григорика).
  • Если время “теряется” между requestStart и responseEnd, стоит рассмотреть возможность использования CDN для доставки вашего клиентского кода. Особенно важно не заострять внимание на среднем сроке доставки, а проверить наличие пользователей с самой высокой задержкой и соотнести это с некоторыми данными о местоположении.
  • Проверьте список ресурсов и уберите те, которые вам не нужны.
  • Если на построение DOM уходит слишком много времени, проверьте порядок, в котором у вас загружаются ресурсы. HTML-документ и таблицы стилей должны уже быть на месте, прежде чем начнется построение DOM. Ситуацию можно улучшить, изменив порядок загрузки ресурсов. Если вы используете такие фреймворки, как Angular, React или Vue, рассмотрите возможность использования рендеринга на стороне сервера, чтобы на стороне пользователя некоторые элементы отображались быстрее.

Скорость — это тоже фича. Проследите за своим приложением в реальных условиях, чтобы увидеть, где и как его можно сделать быстрее. Многие метрики уже имеются в вашем распоряжении благодаря API JavaSсript, а другие API позволяют вам настроить свои собственные. Собирая и извлекая полезную информацию из этих данных, вы можете увидеть, где теряется время, и спроектировать решения.

Читайте также:


Перевод статьи Dornhoth: Improve your Website’s Performance with Real User Monitoring”