Системный подход к скорости: онлайн-измерения на фронтенде

Команда скорости Яндекса вручную оптимизирует поисковую выдачу. Делать это вслепую трудно и зачастую просто бесполезно. Поэтому в компании построили инфраструктуру для сбора метрик, тестирования скорости и анализа полученных данных.

О том, какие метрики стоит использовать и как все оптимизировать, знает разработчик интерфейсов Яндекса Андрей Прокопюк (Andre_487).

jhgnr48w9fhjphnfjkjcggtsdds.png

В основе материала — выступление Андрея на конференции HolyJS. Под катом — и видеозапись, и текстовая версия доклада.


В пару к этому докладу об онлайн-измерениях есть доклад Алексея Калмакова (тоже из Яндекса) об офлайн-измерениях, в его случае нет текстовой версии, но доступна видеозапись.

Поисковая выдача Яндекса состоит из множества различных блоков, классов ответов на пользовательские запросы. Над ними в компании работают более 50 человек, и чтобы скорость выдачи не падала, мы постоянно присматриваем за разработкой.

0444e1e2a6db39e37b9c2a085c7433db.jpg

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

Чтобы ответить на этот вопрос, расскажу две истории.

История внедрения специфичного веб-шрифта на выдаче


Устроив эксперимент с шрифтами, мы обнаружили, что среднее время отрисовки контента ухудшилось на 3%, на 62 миллисекунды. Не так уж много, если принять это за дельту в вакууме. Заметная невооруженным глазом задержка начинается только со 100 миллисекунд — и все же время до первого клика сразу увеличилось на полтора процента.

4ef2b5548ae1789980552312d0b92857.jpg

Пользователи стали позже взаимодействовать со страницей. Почти на полпроцента уменьшилось число кликнутых страниц. Сократилось время присутствия на сервисе и увеличилось время отсутствия.

c15215b5fb72633582890da9e018d4a4.jpg

Фичу со шрифтами мы выкатывать не стали. Ведь эти цифры кажутся маленькими, пока не вспомнишь о масштабе сервиса. В реальности полтора процента — сотни тысяч человек.

Кроме того, скорость имеет накопительный эффект. За одним обновлением с долей некликнутых — 0,4% последуют еще и еще. В Яндексе подобные фичи выкатываются десятками в день, и если не бороться за каждую долю, недолго докатиться и до 10%.

История кеширования в LS


Эта история связана с тем, что мы инлайним в страницу много статического контента.

c333fd7d205cc0695c8934cb0111c820.jpg

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

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

Тогда мы ориентировались главным образом на метрики «размер HTML» и «время доставки HTML» и получили по ним хорошие результаты. Шло время, мы изобрели новые способы измерения скорости, набрались опыта и решили перепроверить, провести обратный эксперимент, отключив оптимизацию.

3b937c2c9c4f0a85c2e76a26d6fdfacf.jpg

Среднее время доставки HTML (ключевая на момент разработки оптимизации метрика) увеличилось на 12%, что очень много. Но при том улучшилось время до отрисовки шапки, до начала парсинга контента и до инициализации JavaScript. Также сократилось время до первого клика. Процент по нему небольшой — 0,6, но если вспомнить о масштабах…

f8835c0fdce5780914f129f41f445f8d.jpg

Отключив оптимизацию, мы получили ухудшение по метрике, заметное только специалистам, и одновременно — улучшение, заметное пользователю.

Из этого можно сделать следующие выводы:

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

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

0bf2c606bacfb7bc5769594fbc326a52.jpg

Когда-то Эш из зловещих мертвецов учил нас сначала стрелять, потом думать или вообще не думать. В скорости так делать не надо.

И третий момент: измерения должны отражать пользовательский опыт. Например, размер HTML и время его доставки — плохие метрики скорости, потому что пользователь не сидит с devTools и не выбирает сервис с меньшей задержкой. А вот какие метрики хорошие и правильные — расскажем дальше.

Что и как измерить?


Измерения стоит начать с нескольких ключевых метрик, которые, в отличие, например, от размера HTML, близки к пользовательскому опыту.

ec13c57830d7b1b3b8380273dda5bbc8.jpg

Если TTFCP (time to first contentfull paint) и TTFMP (time to first meaningful paint) обозначают время до первой отрисовки контента и время до отрисовки значимого контента, то третью — время до инициализации фреймворка, стоит пояснить.

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

И последняя, четвертая метрика, время до первой интерактивности, обычно обозначается как time to interactive (TTI).

Эти метрики, в отличие от размера html или времени его доставки, близки к пользовательскому опыту.

Time to firstcontentfull paint


Для измерения времени, когда пользователь увидел на странице первый контент, существует Paint Timing API, пока доступный только в хромиуме. Данные из него можно получить следующим способом.

e1680ac270e35eaad20070c90e47066d.jpg

Таким вызовом мы получаем набор событий об отрисовке. Пока поддерживается два типа событий: first paint — любая отрисовка и firstcontentfull paint — любая отрисовка контента, отличного от белого фона пустой вкладки и контента бэкграунда страницы.

Так мы получаем массив событий, фильтруем firstcontentfull paint и отправляем с неким ID.

Time to first meaningful paint


В Paint Timing API нет события, сигнализирующего, что на странице отрисовался значимый контент. Это связано с тем, что такой контент на каждой странице свой. Если речь о видеосервисе, то главное — плеер, в поисковой выдаче — первый нерекламный результат. Сервисов великое множество, и универсальный API пока что не разработан. Но тут в ход идут хорошие, проверенные костыли.

В Яндексе есть две школы костылей по измерению этой метрики: использование RequestAnimationFrame и измерение с помощью InterceptionObserver.

В RequestAnimationFrame отрисовка измеряется с помощью интервала.

22bdfe945df84617220936267638cc21.jpg

Допустим, есть некий значимый контент. Здесь это div с классом main-content. Перед ним размещается скрипт, где дважды вызывается RequestAnimationFrame.

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

79d783a883b6a718095228cfbea29469.jpg

Первым идет выполнение JavaScript, затем — разбор стилей, потом расчет Layout, отрисовка и композиция.

Сallback, вызывающий RequestAnimationFrame, активируется на том же этапе, что и JavaScript, а контент отрисовывается в последнем отрезке кадра при композиции. Поэтому в первом вызове мы получаем только нижнюю границу, заметно удаленную во времени от вывода пикселей на экран.

81c788f38c68e241cf3a34612503f9d0.jpg

Расположим два кадра рядом. Видно, что в конце первого из них отрисовался контент. Записываем нижнюю границу RequestAnimationFrame, вызванного внутри первого callback, и вызываем callback во втором кадре. Таким образом получаем интервал от JavaScript, вызванного в том кадре, где рендерился контент, и до JavaScript во втором кадре.

InterceptionObserver


Наш второй костыль с тем же контентом работает по-другому. На этот раз скрипт помещается ниже. В нем мы создаем InterceptionObserver и подписываемся на domNode.

fd6dea87dd53e374e2ba19aa79d4a379.jpg

При этом дополнительных параметров не передаем, поэтому измеряем его пересечение с viewport. Это время и записываем как точное время отрисовки.

5495abd0268d0d59077d00ade282989a.jpg

Это работает потому, что пересечением основного контента и viewport считается именно то пересечение, которое видит пользователь. Этот API был разработан, чтобы точно знать, когда пользователь увидел рекламу, но наши исследования показали, что на нерекламных блоках это тоже работает.

Из двух этих способов все же лучше использовать RequestAnimationFrame: его поддержка шире, и он лучше проверен нами на практике.

JS Inited


Представьте некий фреймворк, у которого есть некое событие «init», на которое можно подписаться, но помните, что на практике JS Inited — одновременно и простая, и сложная метрика.

3bf6fe85c743f0ad6c255d8e4c448eae.jpg

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

Time to Interactive


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

Измерить это помогает концепция долгих (длинных) задач и Long Task API.

Сначала о долгих задачах.

f7967b8283a7302b1d8d9b1bd076cfac.jpg

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

Пользователю придется дождаться, пока они закончатся, и только после браузер поставит на выполнение обработку его ввода. При этом фреймворк уже может быть проинициализирован, а кнопочки будут работать, но медленно. Такой отложенный ответ — довольно неприятный пользовательский опыт. Момент, когда завершен последний Long Task и поток продолжительное время пустует, на иллюстрации наступает на 7 секундах и 300 миллисекундах.

Как измерить этот интервал внутри JavaScript?

66ff1b114d718aa4e31cf8c2f1094525.jpg

Первый шаг условно обозначен как открывающийся тег body, после которого идет script. Здесь создается PerformanceObserver, который подписан на событие Long Task. Внутри callback PerformanceObserver информация о событиях собирается в массив.

3e31c4c60298c19431ca84a93c908394.jpg

После сбора данных наступает время для второго шага. Он условно обозначен как закрывающийся тег body. Мы берем последний элемент массива, последнюю долгую задачу, смотрим на момент окончания ее выполнения и проверяем, достаточно ли прошло времени.

В оригинальной работе по этой метрике в роли константы взяты 5 секунд, но выбор никак не обоснован. Нам оказалось достаточно 3 секунд. Если проходит 3 секунды, мы засчитываем время до первой интерактивности, если нет, делаем setTimeout и проверяем на эту константу повторно.

Как обработать данные?


Данные нужно получить от клиентов, обработать и представить в удобном виде. Наша концепция отправки данных достаточно проста. Она называется счетчик.

38f158cbaf1233223fdca1ba255f9e25.jpg

Мы передаём данные определённой метрики на специальную ручку на бэкенде и собираем их в хранилище.

884cda04a3b619b084dfde376a0645cc.jpg

Здесь агрегация данных условно обозначена как SQL-запрос. Тут же приведены основные агрегации, которые мы обычно считаем по скоростным метрикам: среднее арифметическое и группа процентилей (50-й, 75-й, 95-й, 99-й).

35484ad7c51d967118abb42646a09801.jpg

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

d6b2acb00b844c6294fa80a50e03495c.jpg

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

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

Но та же чувствительность — преимущество, когда речь идет о мониторинге. Чтобы точно не пропустить важную проблему, мы используем среднее арифметическое. Оно легко смещается, но риск ложного срабатывания в данном случае — не такая уж большая проблема. Лучше переглядеть, чем недоглядеть.

dcbe92a6baf2a4ec1e2ef01580dd948d.jpg

Кроме того, мы считаем медиану (если привязывать это к временным метрикам, медиана — показатель времени, в которое укладывается 50% запросов) и 75-й процентиль. 75% запросов укладываются в это время, его мы принимаем за оценку общей скорости. 95-й и 99-й процентили считаются для оценки длинного медленного хвоста. Это очень большие числа. 95-й рассматривается как самый медленный запрос. На 99-й процентиль приходятся аномальные показатели.

Считать максимум нет смысла. Это путь к безумию. После расчетов максимума может получиться, что пользователь ждал, пока загрузится страница, 20 лет.

Посчитав агрегации, остается только применить эти числа, и самое очевидное, что с ними можно сделать — представить в виде графиков.

e8d5523e9eeebf2545a7f214b75b8ede.jpg

На графике наши реальные метрики time to first contentfull paint для поиска. Синяя линия отражает динамику для десктопов, красная — для мобильных устройств.

За графиками скорости приходится постоянно следить, и мы поручили эту задачу роботу.

Мониторинг


Поскольку метрики скорости волатильны и постоянно колеблются с различными периодами, мониторинг нужно настраивать тонко. Для этого мы используем концепцию разладок.

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

6219a7c0bc99aef5991cda07179a487a.jpg

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

93ed21ec4f2a624e3d703dc90b2c2530.jpg

Желтый график — показатель метрики, а синий — скользящее среднее с достаточно большим периодом. Красный — среднее плюс три стандартных отклонения. Зеленое —  то же самое, только со знаком минус.

4c8899b90f7e2696100aa4c7069a868c.jpg

Красный и зеленый показатели образуют безопасный коридор. Пока метрика и скользящее среднее колеблются между ними — все в норме, это обычные колебания. Но стоит им покинуть безопасную зону — срабатывает мониторинг.

9fbd4125ece689bd90a84e520304fd3a.jpg

Проверка фичей на скорость


Все, о чем шла речь, — работа со скоростными данными уже запущенного проекта, но хочется измерять скорость отдельных фич перед тем, как отправить их в большой продакшн. Для этого мы используем A/Б тестирование — сравнение метрик по контрольной и экспериментальной группам.

819128e94c08fec52e22da5914d28cbe.jpg

Мы делим пользователей на контрольную и экспериментальную группы. Показания каждого слота собираются раздельно, агрегируются и сводятся в таблицу.

31d89b1eb1915e2a0f8f29bc986f0521.jpg

В A/Б тестировании, как правило, также используется среднее арифметическое. Здесь мы видим дельту и, чтобы точно определить, случайность это или значимый результат, применяется статистический тест.

e7837d1340c59e00cf61a132c6ac72c9.jpg

Он обозначен как «MW», потому что при расчете используется критерий Манна-Уитни. С его помощью вычисляется так называемый «процент правоты». У этого показателя есть порог, после которого мы принимаем дельту за верную. Здесь он установлен в 99,9%.

Когда тест достигает этого значения, дельта отмечается в интерфейсе цветом. Мы называем это прокраской. Здесь мы видим зеленую, то есть хорошую прокраску на time to first contentfull paint. Time to first meaningful paint не дотягивает до этого значения, то есть дельта тоже хорошая, но не 99,9%. Доверять ей полностью нельзя. По инициализации фреймворка и time to interactive наблюдается уверенно плохая красная прокраска. Из этого можно сделать такой же вывод, как и в случае со шрифтами.

25b415b14b30b25549f514aced7b52dd.jpg

Как сделать у себя?


Реализовать измерения скорости у себя можно двумя способами. Первый — сделать все свое.

e30344cb56179ed9a633ff26c187291e.jpg

Ручка для приема данных с клиентов, backend, который складывает все это в базу, MongoDB, PostgreSQL, MySQL, любая СУБД (в них из коробки есть агрегации), плюс одно из множества опенсорсных решений — для того чтобы нарисовать графики и устроить мониторинг.

Второе решение — воспользоваться системами аналитики «Яндекс Метрика» или Google Analytics. На примере «Яндекс Метрики» это выглядит так.

424a97f61a797dff9419d7df8828046b.jpg

Здесь приведены показатели, которые метрика предоставляет пользователю из коробки. Конечно, это не все упомянутое, но уже кое что. Остальное можно добавить вручную через пользовательские параметры. Также доступно А/Б тестирование и мониторинги.

Заключение


Концепция онлайн-измерения скорости, о которой мы рассказали, известна под названием RUM — Real User Monitoring. Мы так ее любим, что даже нарисовали логотип с крутым рок-н-ролльным умлаутом.

e5f8a94489f01743c09e5ec04453cc56.jpg

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

Объявление напоследок. Если вам понравился этот доклад с HolyJS 2018 Piter, вероятно, вам будет интересно и на приближающейся HolyJS 2018 Moscow, которая пройдёт 24–25 ноября. Там можно будет не только увидеть многие другие JS-доклады, но и расспросить после доклада любого спикера в дискуссионной зоне. А уже завтра, с 1 ноября, цены на билеты повысятся до финальных, так что сегодня последняя возможность купить их со скидкой!

© Habrahabr.ru