3 ошибки, ведущие к потере производительности в JavaScript

Перевод статьи Йотама Кадишая «3 JavaScript Performance Mistakes You Should Stop Doing».

Потеря производительности в JavaScript, которую можно избежать

Что, если я скажу вам, что все, что вы знаете, это ложь? Что, если все изученные вами ключевые особенности нашего любимого ECMAScript, опубликованные в последние годы, это на самом деле опасные ловушки в плане производительности?

Эта история началась несколько лет назад, в старые добрые времена ES5…

Я все еще очень живо помню, как вышел ES5. В нашем любимом JavaScript были представлены отличные новые функции массивов. Среди них были forEach, reduce, map, filter . Они дали нам почувствовать, что язык растет, становится более функциональным, написание кода становится более интересным и плавным, а сам код в результате – более читаемым и понятным.

Примерно в то же время выросла новая среда –  Node.js, которая дала нам возможность плавного перехода от фронтенда к бэкенду и одновременно полностью переопределила full stack разработку.

Сегодня Node.js, используя ECMAScript на V8, стремится войти в главную лигу языков бэкенд-разработки и поэтому нуждается в доказательствах подобающей производительности. Да, в расчет могут приниматься многие параметры и нет какого-то одного языка, который был бы превосходен во всем. Но помогает или вредит производительности наших приложений написание их на JavaScript с использованием предоставляемого из коробки функционала?

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

Чтобы найти ответы на эти вопросы, я попробовал сравнить несколько сценариев и проанализировать полученные результаты. Я проводил указанные далее тесты на Node.js v10.11.0 и в браузере Chrome, и то, и другое – на macOS.

1. Зацикливание массива

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

Я сравнивал суммирование 10 тысяч рандомных элементов с использованием for, for-of, while, forEach и reduce. Запуск тестов 10 тысяч раз возвращал следующие результаты:

For Loop, average loop time: ~10 microseconds
For-Of, average loop time: ~110 microseconds
ForEach, average loop time: ~77 microseconds
While, average loop time: ~11 microseconds
Reduce, average loop time: ~113 microseconds

Я погуглил, как суммировать массив, и reduce был самым рекомендуемым решением, но также и самым медленным. Мой любимый forEach оказался не намного лучше. Даже новейший for-of (ES6) дает неважную производительность. Оказалось, что старый добрый цикл for (а также while) обогнал их всех – его производительность в 10 раз выше!

Как могут новейшие и рекомендуемые решения так сильно замедлять JavaScript? Причина этогокроется в двух вещах. Во-первых, reduce и forEach запрашивают исполнение функции обратного вызова, которая выполняется рекурсивно и раздувает стэк. Вторая причина – дополнительная операция и проверка, производимая над выполненным кодом (описано здесь).

Высокая скорость работы

2. Дублирование массива

Хотя этот сценарий кажется менее интересным, это основа иммутабельных функций, где при генерации output не изменяется input.

Результаты тестирования производительности опять показали интересную тенденцию. При дублировании 10 тысяч массивов из 10 тысяч рандомных элементов самыми быстрыми оказались олдскульные решения. Самая модная в ES6 операция spread `[…arr]` и Array из `Array.from(arr)` плюс map из ES5 `arr.map(x => x)` оказались хуже, чем ветеран slice `arr.slice()` и concatenate `[].concat(arr)`.

Duplicate using Slice, average: ~367 microseconds
Duplicate using Map, average: ~469 microseconds
Duplicate using Spread, average: ~512 microseconds
Duplicate using Conct, average: ~366 microseconds
Duplicate using Array From, average: ~1,436 microseconds
Duplicate manually, average: ~412 microseconds

3. Итерации объектов

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

Опять есть решения-ветераны, такие как for-in `for(let key in obj)`, или более позднее `Object.keys(obj)` (представлено в ES6) и `Object.entries(obj)` (из ES8), которые возвращают и ключ, и значение.

Анализ производительности итераций 10 тысяч объектов, каждый из которых содержал по тысяче рандомных ключей и значений, с использованием вышеуказанных методов показал следующее:

Object iterate For-In, average: ~240 microseconds
Object iterate Keys For Each, average: ~294 microseconds
Object iterate Entries For-Of, average: ~535 microseconds

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

Итоги

Мои выводы предельно ясны. Если для вашего приложения важна высокая производительность или вашим серверам нужно некоторое управление загрузкой, то самые классные, читаемые и чистые варианты не для вас. Они нанесут удар по производительности приложения, которое может стать чуть ли не в 10 раз медленнее!

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

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

2 комментария к “3 ошибки, ведущие к потере производительности в JavaScript”

  1. В реальных приложениях больше тратится время на операции ввода/вывода — DOM, Network, Storages и т.д. Хватит гонять бесполезные бэнчмарки, которые склоняют делать выбор в сторону менее читаемого когда

  2. 100 микросекунд это много или мало? Смотря для чего. Для того, чтобы делать интерфейсы это слишком мало, чтобы жертвовать читаемостью кода. Автор мудак, статья вредная.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх