Измерение времени, которое уходит на выполнение функции — это хороший способ доказательства того, что одна реализация некоего механизма является более производительной, чем другая. Это позволяет удостовериться в том, что производительность функции не пострадала после неких изменений, внесённых в код. Это, кроме того, помогает искать узкие места производительности приложений.
Если веб-проект обладает высокой производительностью — это вносит вклад в его позитивное восприятие пользователями. А если пользователям понравилось работать с ресурсом — они имеют свойство возвращаться. Например, в этом исследовании показано, что 88% онлайн-клиентов менее склонны возвращаться на ресурсы, при работе с которыми они столкнулись с какими-то неудобствами. Эти неудобства вполне могут быть вызваны проблемами с производительностью.
Именно поэтому в деле веб-разработки важны инструменты, помогающие находить узкие места производительности и измерять результаты улучшений, вносимых в код. Подобные инструменты особенно актуальны в JavaScript-разработке. Здесь важно знать о том, что каждая строка JavaScript-кода может, в потенциале, заблокировать DOM, так как JavaScript — это однопоточный язык.
В этом материале я расскажу о том, как измерять производительность функций, и о том, что делать с результатами измерений.
Если вы полагаете, что некоторые вычисления слишком тяжелы для выполнения их в главном потоке, то вы, возможно, решите переместить их в сервис-воркер или в веб-воркер.
Метод performance.now()
Интерфейс Performance даёт доступ к значению типа DOMHighResTimeStamp через метод performance.now()
. Этот метод возвращает временную метку, указывающую на время в миллисекундах, прошедшее с момента начала существования документа. Причём, точность этого показателя составляет порядка 5 микросекунд (доли миллисекунды).
Для того чтобы измерить производительность фрагмента кода, пользуясь методом performance.now()
, нужно выполнить два измерения времени, сохранить результаты этих измерений в переменных, а затем вычесть из результатов второго измерения результаты первого:
const t0 = performance.now();
for (let i = 0; i < array.length; i++)
{
// какой-то код
}
const t1 = performance.now();
console.log(t1 - t0, 'milliseconds');
В Chrome после выполнения этого кода можно получить примерно такой результат:
0.6350000001020817 "milliseconds"
В Firefox — такой:
1 milliseconds
Как видно, результаты измерений, полученные в разных браузерах, серьёзно различаются. Дело в том, что в Firefox 60 точность результатов, возвращаемых API Performance, снижена. Мы ещё поговорим об этом.
Интерфейс Performance обладает гораздо большими возможностями, чем только возврат некоей временной метки. К ним относятся измерение различных аспектов производительности, представленные такими расширениями этого интерфейса, как API Performance Timeline, Navigation Timing, User Timing, Resource Timing. Вот материал, в котором можно найти подробности об этих API.
В нашем случае речь идёт об измерении производительности функций, поэтому нам достаточно возможностей, которые даёт метод performance.now()
.
Date.now() и performance.now()
Тут у вас может возникнуть мысль о том, что для измерения производительности можно пользоваться и методом Date.now()
. Это и правда возможно, но у такого подхода есть недостатки.
Метод Date.now()
возвращает время в миллисекундах, прошедшее с эпохи Unix (1970-01-01T00:00:00Z) и зависит от системных часов. Это означает не только то, что этот метод не так точен, как performance.now()
, но и то, что он, в отличие от performance.now()
, возвращает значения, которые в определённых условиях могут быть основаны на неправильных показателях часов. Вот что об этом говорит Рич Джентлкор — программист, имеющий отношение к движку WebKit: «Возможно, программисты реже думают о том, что показания, возвращаемые при обращении к Date
, основанные на системном времени, совершенно нельзя назвать идеальными для мониторинга реальных приложений. В большинстве систем работает демон, который регулярно синхронизирует время. Подстройка системных часов на несколько миллисекунд каждые 15-20 минут — это обычное дело. При такой частоте настройки часов около 1% измерений 10-секундных интервалов окажутся неточными».
Метод console.time()
Измерение времени с использованием этого API производится крайне просто. Достаточно, перед кодом, производительность которого нужно оценить, вызвать метод console.time()
, а после этого кода — метод console.timeEnd()
. При этом и тому и другому методам нужно передать один и тот же строковой аргумент. На одной странице одновременно можно использовать до 10000 подобных таймеров.
Точность измерений времени, производимых с помощью этого API, такая же, как и при использовании API Performance, но то, какой именно точности удастся достичь в каждой конкретной ситуации, зависит от браузера.
console.time('test');
for (let i = 0; i < array.length; i++) {
// какой-то код
}
console.timeEnd('test');
После выполнения подобного кода система автоматически выведет в консоль сведения о прошедшем времени.
В Chrome это будет выглядеть примерно так:
test: 0.766845703125ms
В Firefox — так:
test: 2ms - timer ended
Собственно говоря, тут всё очень похоже на то, что мы видели, работая с performance.now()
.
Сильная сторона метода console.time()
заключается в простоте его использования. А именно, речь идёт о том, что его применение не требует объявления вспомогательных переменных и нахождения разницы между записанными в них показателями.
Сниженная точность временных показателей
Если вы, пользуясь вышеописанными средствами, измеряли производительность своего кода в разных браузерах, то вы могли обратить внимание на то, что результаты измерений могут различаться.
Причиной этого является то, что браузеры пытаются защитить пользователей от атак, основанных на анализе времени, и от механизмов идентификации браузеров (Browser Fingerprinting). Если результаты измерения времени окажутся слишком точными, это может дать злоумышленникам возможность, например, идентифицировать пользователей.
В Firefox 60, как уже было сказано, точность результатов измерения времени снижена. Это сделано с помощью установки значения свойства privacy.reduceTimerPrecision
в значение 2 мс.
Кое-что, о чём стоит помнить, тестируя производительность
Теперь в вашем распоряжении есть инструменты, позволяющие измерять производительность JavaScript-функций. Но, прежде чем приняться за дело, нужно учитывать некоторые особенности, о которых мы сейчас поговорим.
▍Разделяй и властвуй
Предположим, что вы, фильтруя некие данные, обратили внимание на медленную работу приложения. Но вы не знаете о том, где именно находится узкое место производительности.
Вместо того чтобы строить догадки о том, какая именно часть кода работает медленно, лучше будет выяснить это, воспользовавшись вышеописанными методиками.
Для того чтобы увидеть общую картину происходящего, сначала стоит, воспользовавшись console.time()
и console.timeEnd()
, оценить производительность блока кода, который, предположительно, плохо влияет на производительность. Затем надо посмотреть на скорость работы отдельных частей этого блока. Если одна из них выглядит заметно медленнее, чем другие, нужно уделить ей особое внимание и как следует её проанализировать.
Чем меньше кода находится между вызовами методов, измеряющих время, тем ниже вероятность того, что измеряться будет что-то, не имеющее отношения к проблемной ситуации.
▍Учитывайте особенности поведения функций при разных входных значениях
В реальных приложениях данные, поступающие на вход конкретной функции, могут быть очень разными. Если измерить производительность функции, которой передали некий произвольно выбранный набор данных, это не даст никаких ценных сведений, способных прояснить происходящее.
Функции при исследовании производительности нужно вызывать с входными данными, максимально напоминающими реальные.
▍Запускайте функции по много раз
Предположим, у вас имеется функция, которая перебирает массив. Она выполняет некие вычисления, используя каждый элемент массива, а после этого возвращает новый массив с результатами вычислений. Вы, размышляя об оптимизации этой функции, хотите узнать о том, что в вашей ситуации работает быстрее — цикл forEach
или обычный цикл for
.
Вот два варианта подобной функции:
function testForEach(x) {
console.time('test-forEach');
const res = [];
x.forEach((value, index) => {
res.push(value / 1.2 * 0.1);
});
console.timeEnd('test-forEach')
return res;
}
function testFor(x) {
console.time('test-for');
const res = [];
for (let i = 0; i < x.length; i ++) {
res.push(x[i] / 1.2 * 0.1);
}
console.timeEnd('test-for')
return res;
}
Протестируем функции:
const x = new Array(100000).fill(Math.random());
testForEach(x);
testFor(x);
После запуска кода мы получаем следующие результаты:
test-forEach: 27ms - timer ended
test-for: 3ms - timer ended
Похоже, цикл forEach
оказался гораздо медленнее цикла for
. Ведь результаты тестирования указывают именно на это?
На самом деле, после однократного испытания рано делать подобные выводы. Попробуем вызвать функции по два раза:
testForEach(x);
testForEach(x);
testFor(x);
testFor(x);
Получим следующее:
test-forEach: 13ms - timer ended
test-forEach: 2ms - timer ended
test-for: 1ms - timer ended
test-for: 3ms - timer ended
Получается, что функция, в которой используется forEach
, вызванная второй раз, оказывается такой же быстрой, как и та, в которой применяется for
. Но, учитывая то, что при первом вызове forEach
-функция работает заметно медленнее, её, возможно, всё же использовать не стоит.
▍Тестируйте производительность в разных браузерах
Вышеприведённые тесты выполнялись в Firefox. А что если выполнить их в Chrome? Результаты будут совсем другими:
test-forEach: 6.156005859375ms
test-forEach: 8.01416015625ms
test-for: 4.371337890625ms
test-for: 4.31298828125ms
Дело в том, что браузеры Chrome и Firefox основаны на разных JavaScript-движках, в которых реализованы разные оптимизации производительности. Об этих различиях весьма полезно знать.
В данном случае в Firefox наблюдается лучшая оптимизация forEach
при сходных входных данных. А цикл for
оказывается быстрее чем forEach
и в Chrome, и в Firefox. В результате, вероятно, лучше остановиться именно на варианте функции с for
.
Это — хороший пример, демонстрирующий важность измерения производительности в разных браузерах. Если оценить производительность некоего кода только в Chrome, то можно прийти к выводу о том, что цикл forEach
, в сравнении с циклом for
, не так уж и плох.
▍Применяйте искусственные ограничения системных ресурсов
Значения, которые получены в наших экспериментах, не выглядят особенно большими. Но знайте о том, что компьютеры, которые используют для разработки, обычно гораздо быстрее чем, скажем, средний мобильный телефон, на котором просматривают веб-страницы.
Для того чтобы поставить себя на место пользователя, обладающего не самым быстрым устройством, воспользуйтесь возможностями браузера по искусственному ограничению системных ресурсов. Например — по снижению производительности процессора.
При таком подходе 10 или 50 миллисекунд легко могут превратиться в 500.
▍Измеряйте относительную производительность
Результаты измерений производительности обычно зависят не только от аппаратного обеспечения, но и от текущей нагрузки на процессор, и от загруженности главного потока JavaScript-приложения. Поэтому постарайтесь опираться на относительные показатели, характеризующие изменение производительности, так как абсолютные показатели, полученные при анализе одного и того же фрагмента кода в разное время, могут сильно различаться.
Итоги
В этом материале мы рассмотрели некоторые JavaScript-API, предназначенные для измерения производительности. Мы поговорили и о том, как использовать их для анализа реального кода. Я полагаю, что для того чтобы выполнить какие-то простые измерения, проще всего пользоваться console.time()
.
У меня есть такое ощущение, что многие фронтенд-разработчики не уделяют достаточно внимания измерению производительности своих проектов. А им стоило бы постоянно следить за соответствующими показателями, так как производительность влияет на успешность и прибыльность проектов.
Уважаемые читатели! Если вы постоянно контролируете производительность своих проектов, просим рассказать о том, как вы это делаете.
Автор: ru_vds