Skip to main content

Методи масиву проти циклів

Методи масивів (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, важкі агрегації.

Швидкий приклад

javascript
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');

На більшості рушіїв результат буде приблизно такий:

text
map: 30-60 ms for: 10-20 ms

Що відбувається під капотом

Коли ви пишете:

javascript
arr.map(x => x * 2);

рушій робить приблизно таке:

  1. Перевіряє, що arr справді масив.
  2. Створює новий масив для результатів.
  3. На кожній ітерації:
    • викликає callback-функцію;
    • передає їй три аргументи (element, index, array);
    • зберігає результат у новий масив.
  4. Повертає підсумковий масив.

Це купа кроків, які додають накладні витрати порівняно зі звичайним 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: це декларативно й зрозуміло.

javascript
{items.map(item => <Card key={item.id} {...item} />)}

Але якщо у вас масив на мільйони елементів або цикл у «гарячому місці» (рендеринг, обробка бінарних даних, canvas, парсер), краще взяти:

javascript
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.