3 главные ошибки, которые вредят производительности JavaScript
Рассмотрим 3 главные ошибки, вызывающие спад производительности JavaScript кода, тестируем и избавляемся от "вредных привычек".
Что если ключевые функции ECMAScript, которые вы так кропотливо изучали, – не что иное, как опасная ловушка для снижения производительности JavaScript, красиво упакованная в однострочный функциональный код обратных вызовов?
Эта история началась с выходом ES5 и новых функций массивов (forEach
, reduce
, map
, filter
и т. д.). С этими функциями возрастала функциональность языка. А написание кода становилось более слаженным и интересным. Да и результат работы выглядел проще и понятнее.
Тогда же активно развивалась и другая среда – Node.js. Она в корне пересматривала всю концепцию full-stack разработки и предлагала более плавный переход от front-end к back-end.
Поэтому Node.js с последним ECMAScript в V8 пытается выбиться в лидеры главных серверных языков программирования. А для этого он должен обладать высокой производительностью. Конечно же, нужно учитывать и множество других параметров. И да, универсального языка еще не придумали.
Так все же, JavaScript с его готовыми решениями – это польза или вред для производительности приложений?
С ростом мощности компьютеров и ускорением сети клиентская часть JavaScript становится отличным решением не только для представления/просмотра. Но можно ли положиться на JavaScript при написании высокопроизводительных приложений со сложной архитектурой?
Чтобы ответить на этот вопрос, проанализируем результаты тестирования нескольких операций (зацикливание, дублирование массива и перебор объектов). Все тесты проводились на macOS в Node.js v10.11.0 и браузере Chrome.
1. Зацикливание массива
Первое, что приходит на ум, – это найти сумму массива с 10 тыс. элементами. Пример взят из реальной практики, когда нужно извлечь огромную таблицу из базы данных и просуммировать ее значения, не создавая лишнего запроса к базе данных.
При суммировании случайных 10 тыс. элементов через операторы for
, for-of
, while
, forEach
и reduce
10 тыс. раз. получились значения ниже:
For Loop, среднее время цикла: ~10 микросекунд For-Of, среднее время цикла: ~110 микросекунд ForEach, среднее время цикла: ~77 микросекунд While, среднее время цикла: ~11 микросекунд Reduce, среднее время цикла: ~113 микросекунд
Если погуглить, как правильно суммировать массивы, то самым популярным советом будет использование оператора reduce
. Однако данное решение является крайне медленными. Попытки реализовать задачу через forEach
также не увенчались успехом. Даже в новейшем for-of
(ES6) хромает производительность.
Оказалось, что лидерами по части производительности (в 10 раз быстрее остальных!) стали проверенные временем while
и for
.
Как же получилось, что новейшее и разрекламированное решение настолько сильно тормозит JavaScript? Дело тут в следующем: reduce
и forEach
требуют выполнения функции обратного вызова. Данная функция вызывается рекурсивно и заполняет стэк. Кроме того, торможение обусловлено и дополнительной операцией, а также проверкой исполняемого кода (см. тут).
2. Дублирование массива
Вроде бы ничего интересного в данном пункте нет. Но это основа неизменяемых функций, для которых выходные данные генерируются без изменения входных значений.
При тестировании производительности получается интересный результат: дублирование 10 тыс. массивов из 10 тыс. случайных элементов лучше проводить классическими способами. Новомодная операция spread из […arr]
, Array из Array.from(arr)
в ES6 и map arr.map(x => x)
из ES5 позорно проигрывали "старичкам" slice arr.slice()
и concatenate [].concat(arr)
.
Дублирование через Slice, среднее время: ~367 микросекунд Дублирование через Map, среднее время: ~469 микросекунд Дублирование через Spread, среднее время: ~512 микросекунд Дублирование через Conct, среднее время: ~366 микросекунд Дублирование через Array From, среднее время: ~1,436 микросекунд Дублирование вручную, среднее время: ~412 микросекунд
3. Перебор объектов
Еще одна популярная тема – перебор (итерация) объектов. Он нужен при переборе элементов JSON без поиска какого-либо конкретного значения ключа. Здесь тоже есть свои почетные ветераны – for-in for(let key in obj)
или Object.keys(obj)
(в ES6). Стоит вспомнить и Object.entries(obj)
(в ES8), возвращающий ключи и значения.
Результаты тестирования 10 тыс. итераций объектов из 1 тыс. случайных ключей и значений в каждом:
Итерация объекта через For-In, среднее время: ~240 микросекунд Итерация каждого объекта с получением ключей, среднее время: ~294 микросекунд Итерация объекта через For-Of, среднее время: ~535 микросекунд
В первых двух вариантах вместо прямого перебора объекта без ключей создается перечислимый массив значений. Последний вариант весьма сомнительный.
Заключение
Если для приложения важна высокая производительность, или серверы сталкиваются с существенной нагрузкой, то забудьте про все популярные/легко читаемые/чистые решения. Они приводят к спаду производительности JavaScript кода в 10 раз!
Прекращайте бездумно следовать популярным трендам. Подбирайте решения в соответствии с собственными целями. Для небольших приложений идеально подходит легкий и читабельный код. Для высоконагруженных серверов и приложений с объемной клиентской частью выбирайте что-то другое.
Перевод статьи Yotam Kadishay: 3 JavaScript Performance Mistakes You Should Stop Doing