Данная статья была написана 21 января 2014 участником проекта HTML5Rocks, Алексом Данило. Хочу отметить, что это мой первый перевод на Хабре, поэтому к минусам я готов, к критике еще больше. По всем проблемам и недочетам старайтесь писать мне лично, я буду оперативно исправлять. Оригинал на английском.
Высокая производительность веб-приложений является ключевым моментом для конечного пользователя. Так как веб-приложения становятся всё более и более сложными, понимание управления производительностью является убедительным навыком программиста. За последние пару лет появилось множество новых API в браузере, позволяющих следить за производительностью сети, временем загрузки и т.д. но ни одно из них не позволяет достаточно гибко и детально определить, что же именно “тормозит” ваше приложение. Для этого и есть User Timing API, которое предоставляет механизм для вашего приложения, позволяющий определить, на что и когда расходуется время.
Первым делом в ускорении медленного веб-приложения нам необходимо разобраться с местами, выполнение которых занимает больше всего времени. Измерение времени выполнения отдельных участков Javascript кода — идеальный способ для выявления горячих точек. Это первый шаг в поиске пути улучшения производительности. К счастью, User Timing API предоставляет решение, с помощью которого вы можете вызывать API в различных местах вашего кода для получения детальных данных о времени, которые помогут вам в оптимизации.
Основополагающей частью измерения времени является точность. Раньше мы обходились погрешностью измерения в 1 миллисекунду, но появились jank-free сайты, в которых каждый кадр должен быть отрисован за 16мс. Поэтому для хорошего анализа миллисекундной погрешности недостаточно. Но теперь появился High Resolution Time, новый тип синхронизации, встроенный в современные браузеры. High Resolution Time предоставляет нам временные метки с плавающей точкой с точностью до микросекунды, что в тысячу раз лучше, чем было раньше.
Для получения текущего времени в приложении необходимо вызвать метод now(), который является расширением интерфейса Performance.
Существует еще один интерфейс, называемый PerformanceTiming, который предоставляет набор временных точек, связанных с тем, как загружается веб-приложение. Метод now() возвращает время, прошедшее с момента navigationStart из PerformanceTiming.
Раньше, чтобы получить текущее время приложения, использовалась функция Date.now(), которая возвращала DOMTimeStamp. DOMTimeStamp возвращал целое число миллисекунд. Чтобы предоставить более высокую точность для High Resolution Time, был введен новый тип с названием DOMHighResTimeStamp. Этот тип уже имеет плавающую точку, хотя также возвращает время в миллисекундах. Но, благодаря нецелому значению, время содержит дробные миллисекунды и дает точность до одной тысячной миллисекунды.
Так как теперь мы имеем высокоточное время, можно начать использовать User Timing для получения информации о времени.
Интерфейс User Timing предоставляет нам функции, которые можно вызывать в разных местах приложения для выявления слабых мест.
Этот метод является основным в нашем инструментарии анализа времени. Что же делает mark()? Этот метод сохраняет время для будущего использования. Но что самое полезное в нем, так это то, что мы можем давать имена нашим отметкам. В этом случае API сохраняет связь имя-время как отдельный элемент.
Используя mark() в различных местах, можно определить, сколько времени прошло между вызовами.
Спецификацией были предусмотрены стандартные названия отметок, которые довольно понятны, например: 'mark_fully_loaded','mark_fully_visible','mark_above_the_fold' и т. д.
Для примера, мы могли бы сделатьотметку, когда приложение полностью загрузится:
Устанавливая именованные метки по всему приложению, мы можем собрать огромное количество данных и проанализировать их на досуге, чтобы разобраться, когда и на что тратить время приложение.
После того как вы установите временные отметки, вам необходимо вычислить время между ними. Для этого и нужен метод measure(). Он определяет время, которое произошло между отметками. А также может определить время между пользовательской отметкой и одной из стандартных, перечисленных в интерфейсе Performance Timing. Например, чтобы посчитать время с момента загрузки DOM до того как приложение будет полностью загружено, используется следующий код:
Примечание: в этом примере мы передаем зарезервированное имя 'domComplete' из интерфейса Performance Timing.
Когда вызывается метод measure(), он сохраняет результат независимо от меток, так что вы сможете получить это значение позже. Сохраняя отдельно данные о времени в момент работы приложения, оно остается отзывчивым и работоспособным, и вы сможете выгрузить данные для анализа после того как приложение сделает какую-то работу.
Иногда полезно избавиться от кучи сохраненных отметок. Например, вы можете запускать приложение из командной строки, поэтому логично будет сбросить все накопленные отметки до запуска.
Достаточно легко избавиться от установленных отметок вызовом clearMarks().
Таким образом, следующий пример кода сбросит все существующие отметки, так что вы сможете настроить это заново, если хотите.
Конечно, есть некоторые ситуации, в которых вы бы не хотели очищать все отметки. Если вы хотите удалить конкретные, то вы можете просто передать имя отметки для удаления. Например, следующим образом:
Этот код избавляется от отметки, сохраненной выше. Но все остальные отметки никуда не исчезнут.
Возможно, вы также захотите удалить измерения, которые вы сделали. Для этого есть соответствующий метод clearMeasures(). Он работает точно так же, как clearMarks(). Например:
удалит измерение, которые мы сохранили в примере выше. Для удаления всех измерений необходимо вызывать этот же метод без аргументов, как и в clearMarks().
Это всё хорошо, устанавливать отметки и измерять время, но в какой-то момент вы захотите получить эти данные для проведения анализа. И это тоже очень просто, всё, что вам нужно — это использовать PerformanceTimeline интерфейс.
Например, метод getEntriesByType() позволяет нам получить все отметки или все наши измерения в виде списка, чтобы у нас была возможность пройти по нему и обработать данные. Что удобно, так это то, что список возвращается в хронологическом порядке, и можно следить за отметками по мере того, как они вызывались в приложении.
Следующий код:
возвращает все отметки, которые были отмечены в приложении, в то время как этот код:
возвращает все измерения, которые были произведены.
Также можно получить список записей по имени, которое вы им присваивали:
вернет список с одним элементом содержащим временную метку 'mark_fully_loaded' в свойстве startTime.
Теперь, когда у нас есть полноценная картина о User Timing API, определим как долго выполняются все наши XHR-запросы в приложении.
Сначала нам нужно изменить все send() запросы, чтобы в них устанавливать отметки, а также изменить наши callback-функции, в которых будут устанавливаться другие отметки. А также измерить время между ними, чтобы определить продолжительность запроса.
Обычно, простой XHR запрос выглядит следующим образом:
Для примера мы добавим глобальный счетчик, чтобы следить за количеством запросов а также для использования его в сохранении измерений по каждому запросу. Код для этого будет выглядеть так:
Этот код генерирует измерение с уникальным именем для каждого XHR-запроса, который мы отправляем. Мы предполагаем, что все запросы выполняются последовательно, для параллельных же запросов код нужно немного изменить. Поэтому оставим это как домашнее упражнение для читателей.
После того, как веб-приложение сделало много запросов, мы можем выводить данные в консоль со следующим кодом:
User Timing API дает много отличных инструментов, готовых к применению в разных частях вашего приложения. Уменьшение количества слабых мест может быть легко достигнуто расстановкой вызовов API по всему приложению, и пост-анализом полученных данных. Но что, если Ваш браузер не поддерживает это API? Нет проблем, вы можете найти отличный полифил здесь, который эмулирует API действительно хорошо, и не нагружает систему. Так чего же Вы ждете? Попробуйте User Timing API в ваших приложениях сейчас для того, чтобы ускорить их и пользователи будут благодарны вам.
Высокая производительность веб-приложений является ключевым моментом для конечного пользователя. Так как веб-приложения становятся всё более и более сложными, понимание управления производительностью является убедительным навыком программиста. За последние пару лет появилось множество новых API в браузере, позволяющих следить за производительностью сети, временем загрузки и т.д. но ни одно из них не позволяет достаточно гибко и детально определить, что же именно “тормозит” ваше приложение. Для этого и есть User Timing API, которое предоставляет механизм для вашего приложения, позволяющий определить, на что и когда расходуется время.
Нельзя оптимизировать то, что нельзя измерить
Первым делом в ускорении медленного веб-приложения нам необходимо разобраться с местами, выполнение которых занимает больше всего времени. Измерение времени выполнения отдельных участков Javascript кода — идеальный способ для выявления горячих точек. Это первый шаг в поиске пути улучшения производительности. К счастью, User Timing API предоставляет решение, с помощью которого вы можете вызывать API в различных местах вашего кода для получения детальных данных о времени, которые помогут вам в оптимизации.
High Resolution Time и метод now()
Основополагающей частью измерения времени является точность. Раньше мы обходились погрешностью измерения в 1 миллисекунду, но появились jank-free сайты, в которых каждый кадр должен быть отрисован за 16мс. Поэтому для хорошего анализа миллисекундной погрешности недостаточно. Но теперь появился High Resolution Time, новый тип синхронизации, встроенный в современные браузеры. High Resolution Time предоставляет нам временные метки с плавающей точкой с точностью до микросекунды, что в тысячу раз лучше, чем было раньше.
Для получения текущего времени в приложении необходимо вызвать метод now(), который является расширением интерфейса Performance.
var myTime = window.performance.now();
Существует еще один интерфейс, называемый PerformanceTiming, который предоставляет набор временных точек, связанных с тем, как загружается веб-приложение. Метод now() возвращает время, прошедшее с момента navigationStart из PerformanceTiming.
Тип DOMHighResTimeStamp
Раньше, чтобы получить текущее время приложения, использовалась функция Date.now(), которая возвращала DOMTimeStamp. DOMTimeStamp возвращал целое число миллисекунд. Чтобы предоставить более высокую точность для High Resolution Time, был введен новый тип с названием DOMHighResTimeStamp. Этот тип уже имеет плавающую точку, хотя также возвращает время в миллисекундах. Но, благодаря нецелому значению, время содержит дробные миллисекунды и дает точность до одной тысячной миллисекунды.
Интерфейс User Timing
Так как теперь мы имеем высокоточное время, можно начать использовать User Timing для получения информации о времени.
Интерфейс User Timing предоставляет нам функции, которые можно вызывать в разных местах приложения для выявления слабых мест.
Использование mark()
Этот метод является основным в нашем инструментарии анализа времени. Что же делает mark()? Этот метод сохраняет время для будущего использования. Но что самое полезное в нем, так это то, что мы можем давать имена нашим отметкам. В этом случае API сохраняет связь имя-время как отдельный элемент.
Используя mark() в различных местах, можно определить, сколько времени прошло между вызовами.
Спецификацией были предусмотрены стандартные названия отметок, которые довольно понятны, например: 'mark_fully_loaded','mark_fully_visible','mark_above_the_fold' и т. д.
Для примера, мы могли бы сделатьотметку, когда приложение полностью загрузится:
window.performance.mark('mark_fully_loaded');
Устанавливая именованные метки по всему приложению, мы можем собрать огромное количество данных и проанализировать их на досуге, чтобы разобраться, когда и на что тратить время приложение.
Измерения и метод measure()
После того как вы установите временные отметки, вам необходимо вычислить время между ними. Для этого и нужен метод measure(). Он определяет время, которое произошло между отметками. А также может определить время между пользовательской отметкой и одной из стандартных, перечисленных в интерфейсе Performance Timing. Например, чтобы посчитать время с момента загрузки DOM до того как приложение будет полностью загружено, используется следующий код:
window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');
Примечание: в этом примере мы передаем зарезервированное имя 'domComplete' из интерфейса Performance Timing.
Когда вызывается метод measure(), он сохраняет результат независимо от меток, так что вы сможете получить это значение позже. Сохраняя отдельно данные о времени в момент работы приложения, оно остается отзывчивым и работоспособным, и вы сможете выгрузить данные для анализа после того как приложение сделает какую-то работу.
Сброс отметок с clearMarks()
Иногда полезно избавиться от кучи сохраненных отметок. Например, вы можете запускать приложение из командной строки, поэтому логично будет сбросить все накопленные отметки до запуска.
Достаточно легко избавиться от установленных отметок вызовом clearMarks().
Таким образом, следующий пример кода сбросит все существующие отметки, так что вы сможете настроить это заново, если хотите.
window.performance.clearMarks();
Конечно, есть некоторые ситуации, в которых вы бы не хотели очищать все отметки. Если вы хотите удалить конкретные, то вы можете просто передать имя отметки для удаления. Например, следующим образом:
window.peformance.clearMarks('mark_fully_loaded');
Этот код избавляется от отметки, сохраненной выше. Но все остальные отметки никуда не исчезнут.
Возможно, вы также захотите удалить измерения, которые вы сделали. Для этого есть соответствующий метод clearMeasures(). Он работает точно так же, как clearMarks(). Например:
window.performance.clearMeasures('measure_load_from_dom');
удалит измерение, которые мы сохранили в примере выше. Для удаления всех измерений необходимо вызывать этот же метод без аргументов, как и в clearMarks().
Сохранение данных о времени
Это всё хорошо, устанавливать отметки и измерять время, но в какой-то момент вы захотите получить эти данные для проведения анализа. И это тоже очень просто, всё, что вам нужно — это использовать PerformanceTimeline интерфейс.
Например, метод getEntriesByType() позволяет нам получить все отметки или все наши измерения в виде списка, чтобы у нас была возможность пройти по нему и обработать данные. Что удобно, так это то, что список возвращается в хронологическом порядке, и можно следить за отметками по мере того, как они вызывались в приложении.
Следующий код:
var items = window.performance.getEntriesByType('mark');
возвращает все отметки, которые были отмечены в приложении, в то время как этот код:
var items = window.performance.getEntriesByType('measure');
возвращает все измерения, которые были произведены.
Также можно получить список записей по имени, которое вы им присваивали:
var items = window.performance.getEntriesByName('mark_fully_loaded');
вернет список с одним элементом содержащим временную метку 'mark_fully_loaded' в свойстве startTime.
Пример расчета времени XHR запроса
Теперь, когда у нас есть полноценная картина о User Timing API, определим как долго выполняются все наши XHR-запросы в приложении.
Сначала нам нужно изменить все send() запросы, чтобы в них устанавливать отметки, а также изменить наши callback-функции, в которых будут устанавливаться другие отметки. А также измерить время между ними, чтобы определить продолжительность запроса.
Обычно, простой XHR запрос выглядит следующим образом:
var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
do_something(e.responseText);
}
myReq.send();
Для примера мы добавим глобальный счетчик, чтобы следить за количеством запросов а также для использования его в сохранении измерений по каждому запросу. Код для этого будет выглядеть так:
var reqCount = 0;
var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
window.performance.mark('mark_end_xhr');
reqCnt++;
window.performance.measure('measure_xhr_' + reqCnt, 'mark_start_xhr', 'mark_end_xhr');
do_something(e.responseText);
}
window.performance.mark('mark_start_xhr');
myReq.send();
Этот код генерирует измерение с уникальным именем для каждого XHR-запроса, который мы отправляем. Мы предполагаем, что все запросы выполняются последовательно, для параллельных же запросов код нужно немного изменить. Поэтому оставим это как домашнее упражнение для читателей.
После того, как веб-приложение сделало много запросов, мы можем выводить данные в консоль со следующим кодом:
var items = window.performance.getEntriesByType('measure');
for (var i = 0; i < items.length(); ++i) {
var req = items[i];
console.log('XHR ' + req.name + ' took ' + req.duration + 'ms');
}
Заключение
User Timing API дает много отличных инструментов, готовых к применению в разных частях вашего приложения. Уменьшение количества слабых мест может быть легко достигнуто расстановкой вызовов API по всему приложению, и пост-анализом полученных данных. Но что, если Ваш браузер не поддерживает это API? Нет проблем, вы можете найти отличный полифил здесь, который эмулирует API действительно хорошо, и не нагружает систему. Так чего же Вы ждете? Попробуйте User Timing API в ваших приложениях сейчас для того, чтобы ускорить их и пользователи будут благодарны вам.