Производительность ...spread оператора

В React распространенным методом изменения свойств объектов является применение spread оператора. Его синтаксис подкупает своей простой и понятностью.

Тем не менее, нужно понимать, что, в некоторых случаях, использование spread оператора может заметно сказаться на производительности вашего приложения. Разберем такой пример: мы получаем на входе массив объектов и нам необходимо создать из него один объект вида ключ-значение.

const { faker } = require('@faker-js/faker') // generate fakeData const fakeData = [] for (let i = 0; i < 5; i++) { fakeData.push({ key: faker.datatype.uuid(), name: faker.address.cityName() }) } /* fakeData: [ { key: '0bc9a57c-7fd1-449a-8d98-6396f722535a', name: 'Abilene' }, { key: '2ac57365-bc80-45a1-8033-9efd33de4a52', name: 'Aloha' }, { key: 'a7d64eaa-0202-4c18-ade1-f43b0853c29c', name: 'Johns Creek' }, { key: '129a89a6-490a-48b1-9394-7d143926e7d0', name: 'Chicopee' }, { key: '2d606536-7727-496d-bbee-9663b89f40b9', name: 'Covina' } ] */ // generate object with reduce method const object = fakeData.reduce((acc, { key, name }) => { return { ...acc, [key]: name } }, {}) /* object: { '83e12032-7558-467e-b840-ead992754df4': 'Jackson', '4fe2ce86-b202-4891-8b2f-7fa154b4b448': 'Idaho Falls', 'de1d95c0-3c25-4b8c-9e1d-a8bc20409d45': 'El Centro', 'b54dd7d7-b021-4fc5-9de6-633ee4e240bf': 'Fort Pierce', 'dbee592f-79a0-461a-b477-40ae53f0ff53': 'Palm Springs' } */

Если запустить код, то все сработает очень быстро. Но потенциальная проблема здесь кроется в том, что при каждом цикле reduce происходит полное копирование всего объекта.

Добавим код для простой оценки времени выполнения функции и увеличим размер массива:

... function funcSpread() { return fakeData.reduce((acc, { key, name }) => { return { ...acc, [key]: name } }, {}) } console.time('execution time') funcSpread() console.timeEnd('execution time')

У меня вышли следующие цифры:

  • при длине массива 5, время выполнения составила 0.05 мс
  • 1000 -> 115 мс
  • 5000 -> 2.4 секунды!

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

function funcMutate() { return fakeData.reduce((acc, { key, name }) => { acc[key] = name return acc }, {}) }

Данная функция обработает массив из 5000 объектов уже за 2.5 мс. То есть, в 1000 раз быстрее!

Чуть медленнее, но также довольно быстро, с данной задачей справится метод _.set от популярной библиотеки lodash:

const _ = require('lodash') function funcLodash() { return fakeData.reduce((acc, { key, name }) => { return _.set(acc, key, name) }, {}) }

Время выполнения составило 4.6 мс. Что также довольно быстро.

Рассмотрим другие варианты решения данной задачи, если мы хотим продолжать придерживаться принципа иммутабельности.

Первым делом, я попробовал использовать ramda:

const R = require('ramda') function funcRamda() { return fakeData.reduce((acc, { key, name }) => { return R.assoc(key, name, acc) }, {}) }

Время выполнения составило 1.4 секунды. Что все еще медленно, но все же быстрее, чем работа spread оператора.

Следующая популярная библиотека - immutable.js. Данная библиотека работает со своими структурами данных. Поэтому здесь мы еще дальше отойдем от "чистоты" эксперимента, и будем составлять не объект, а Map от immutable.js.

const { Map } = require('immutable') function funcImmutable() { return fakeData.reduce((acc, { key, name }) => { return acc.set(key, name) }, Map({}))

Результат - 13 мс. Что очень близко к нашему лучшему результату. Узким местом Immutable.js считается перевод полученных данных в обычный объект. Но, в данном случае, на общей производительности это сказалось очень мало:

const mapData = funcImmutable() const object = mapData.toObject()

Здесь мы получим почта такой же результат. Иммутабельные данные очень быстрые!

Последним вариантом будет Immer. Сначала используем прямолинейный подход:

const { produce } = require('immer') function funcImmer() { return fakeData.reduce((acc, { key, name }) => { return produce(acc, draft => { draft[key] = name }) }, {}) }

...и получим катастрофичные 19 секунд! Но если обернуть в producе не каждый шаг, а всю функцию, то можно будет добиться значительного улучшения:

function funcImmer2() { return produce({}, draft => { fakeData.forEach(({ key, name }) => { draft[key] = name }) }) }

результат - 8.5 мс. Это все еще медленнее, чем funcMutate или funcLodash, но довольно близко.

Выводы?

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

Значительного увеличения производительности можно добиться отказавшись от принципа иммутабельности данных. Если этот вариант вам не подходит, то хорошим решением могут быть Immer или Immutable.

11
Начать дискуссию