{"id":14275,"url":"\/distributions\/14275\/click?bit=1&hash=bccbaeb320d3784aa2d1badbee38ca8d11406e8938daaca7e74be177682eb28b","title":"\u041d\u0430 \u0447\u0451\u043c \u0437\u0430\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u044b \u0430\u0432\u0442\u043e?","buttonText":"\u0423\u0437\u043d\u0430\u0442\u044c","imageUuid":"f72066c6-8459-501b-aea6-770cd3ac60a6"}

Производительность ...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.

0
Комментарии
-3 комментариев
Раскрывать всегда