Методи масиву проти циклів
Методи масивів (map, filter, forEach, reduce тощо), це абстракції над циклами, реалізовані всередині рушія JavaScript. Вони зручніші, коротші й читабельніші, але мають додаткові накладні витрати, через які можуть бути повільнішими за прості цикли for, for...of і while, особливо на великих масивах.
Теорія
TL;DR
- Метод масиву на кожній ітерації викликає callback і передає йому три аргументи, а
forпросто виконує тіло циклу. map,filter,slice,concatдодатково виділяють новий масив, тобто нову пам'ять і роботу для GC.- «Голий»
forпередбачуваний, тому JIT-компілятор (V8, SpiderMonkey) оптимізує його легше. - На мільйоні елементів
forбуває у 2-5 разів швидшим; на 10 тисячах різниця це частки мілісекунди. - Для звичайного продуктового коду читабельність важливіша за таку різницю, тому
.map()у JSX цілком доречний. - Оптимізувати варто лише «гарячі» ділянки: рендеринг, парсинг, обробку бінарних даних, canvas, важкі агрегації.
Швидкий приклад
const arr = Array.from({ length: 1_000_000 }, (_, i) => i);
// map
console.time('map');
const doubled1 = arr.map(x => x * 2);
console.timeEnd('map');
// for
console.time('for');
const doubled2 = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
doubled2[i] = arr[i] * 2;
}
console.timeEnd('for');На більшості рушіїв результат буде приблизно такий:
map: 30-60 ms
for: 10-20 msЩо відбувається під капотом
Коли ви пишете:
arr.map(x => x * 2);рушій робить приблизно таке:
- Перевіряє, що
arrсправді масив. - Створює новий масив для результатів.
- На кожній ітерації:
- викликає callback-функцію;
- передає їй три аргументи
(element, index, array); - зберігає результат у новий масив.
- Повертає підсумковий масив.
Це купа кроків, які додають накладні витрати порівняно зі звичайним for-циклом, де ви просто виконуєте інструкції без обгорток і перевірок.
Чому саме повільніше
| Причина | Що відбувається |
|---|---|
| Callback-функція | На кожній ітерації викликається функція, тобто зайві виклики на стеку |
| Створення нового масиву | map, filter, slice, concat створюють копії, це додаткова пам'ять |
| Перевірки та контекст | Метод проходить валідації: довжина, holes (дірки в масиві), прототип, тип |
| Неоптимізовані замикання | Якщо callback захоплює зовнішні змінні, накладних витрат ще більше |
| Функціональні принципи | Ці методи «чисті» й не мутують дані, тому потрібно більше аллокацій |
| Цикли простіше оптимізувати JIT-компілятором | Рушій (V8, SpiderMonkey) швидше оптимізує «голий» for |
Чому це помітно саме в «гарячих» ділянках
Якщо цикл виконується мільйони разів (рендеринг, сортування, агрегації даних, парсинг), накладні витрати на callback починають дорого коштувати.
У таких місцях:
- кожен виклик callback, це новий запис у call stack;
- зайві аллокації, це більше роботи для GC (garbage collector);
- додаткові аргументи (
index,array) означають більше об'єктів у пам'яті.
Коли це не має значення
Для більшості бізнес-задач (списки, фільтри, маппінг до 10 тисяч елементів):
- різниця між
forіmap, це частки мілісекунди; - читабельність і чистота коду важливіші.
Саме тому в React/Vue-коді використовують .map() для JSX: це декларативно й зрозуміло.
{items.map(item => <Card key={item.id} {...item} />)}Але якщо у вас масив на мільйони елементів або цикл у «гарячому місці» (рендеринг, обробка бінарних даних, canvas, парсер), краще взяти:
for (let i = 0; i < n; i++) { /* ... */ }Що реально швидше, за порядком
| Цикл | Швидкість | Особливості |
|---|---|---|
for (let i = 0; i < n; i++) | Найшвидший | Без перевірок, inline, передбачуваний |
for...of | Швидкий, але з ітератором | Трохи накладних витрат |
while | Приблизно як for | Залежить від рушія |
forEach() | Повільніший (callback) | Не повертає новий масив |
map() | Повільніший | Створює новий масив |
filter(), reduce() | Ще повільніші | Аллокації, додаткові операції |
Як пришвидшити методи масивів
| Метод | Оптимізація |
|---|---|
.map() | Використовувати чисту стрілкову функцію без зовнішніх замикань |
.filter() | Не будувати ланцюжки .map().filter().reduce(), а об'єднати їх в один прохід |
.reduce() | Для складних операцій винести накопичення у for |
.forEach() | Замінити на for або for...of у «гарячому» коді |
.concat() / spread ([...a, ...b]) | На великих масивах замінити на push.apply() або цикл |
Коротке резюме причин:
| Причина | Чому повільніше |
|---|---|
| Callback-функції | Створюють додаткові виклики й контекст |
| Новий масив | Виділяється нова пам'ять |
| Валідації та ітерація | Вбудовані перевірки та протоколи |
| Навантаження на GC | Створюються тимчасові об'єкти |
| Цикли простіші | Легше оптимізувати JIT-компілятору |
Висновок: для критичних за продуктивністю задач беріть for, для зрозумілого декларативного коду, map, filter, reduce.
Типові помилки
- Переписувати весь код на
for«заради швидкості». Без вимірювання це втрата читабельності без виграшу: у 99% місць різниця непомітна. - Ланцюжки
.map().filter().reduce()на великих масивах. Кожна ланка проходить масив повністю й виділяє проміжний масив. Один цикл або одинreduceробить те саме за один прохід. - Плутати
forEachіforу питанні виходу з циклу. ЗforEachне можна зробитиbreakчиreturnназовні; спроба обійти це черезthrowдорожча за звичайний цикл. - Думати, що
for...ofтакий самий швидкий, як класичнийfor. Він працює через протокол ітератора й на кожному кроці створює об'єкт{ value, done }. - Забувати попередньо виділити масив.
new Array(n)з присвоєнням за індексом дешевше, ніжpush()у цикл на мільйони елементів. - Мікрооптимізувати замість того, щоб змінити алгоритм. Заміна O(n²) на O(n) дає більше, ніж будь-яка заміна
mapнаfor.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.