Насколько важен порядок свойств в объектах JavaScript?
В случае JavaScript-движка V8 — очень даже. В этой статье я привожу результаты своего маленького исследования эффективности одной из внутренних оптимизаций V8.
Описание механизма V8
Прежде чем продолжить чтение, я советую вам прочитать О внутреннем устройстве V8 и оптимизации кода чтобы лучше понимать суть происходящего.
Если очень упростить, то V8 строит объекты используя внутренние скрытые классы. Каждый такой класс соответствует уникальной структуре объекта. Например, если у нас есть такой код:
// Создаётся базовый скрытый класс, с условным названием C0
const obj = {}
// Создается новый класс с описанием поля "a", условным названием С1 и ссылкой на С0
obj.a = 1
// Создается новый класс с описанием поля "b", условным названием С2 и ссылкой на С1
obj.b = 2
// Создается новый класс с описанием поля "c", условным названием С3 и ссылкой на С2
obj.c = 3
Движок кэширует эти цепочки классов, и если вы повторно создадите объект с такими же именами полей и таким же порядком, то он не будет создавать скрытые классы заново, а будет использовать существующие. При этом сами значения свойств могут отличаться.
// Базовый класс уже существует — C0
const obj2 = {}
// Класс описывающий поле "a" и ссылающийся на C0 уже существует — С1
obj2.a = 4
// Класс описывающий поле "b" и ссылающийся на C1 уже существует — С2
obj2.b = 5
// Класс описывающий поле "c" и ссылающийся на C2 уже существует — С3
obj2.c = 6
Тем не менее это поведение легко сломать. Достаточно определить такой же объект, но с полями в другом порядке
// Базовый класс уже существует — C0
const obj3 = {}
// Не существует класса описывающего поле "c" и ссылающегося на C0.
// Будет создан новый — С4
obj3.c = 7
// Не существует класса описывающего поле "a" и ссылающегося на C4.
// Будет создан новый — С5
obj3.a = 8
// Не существует класса описывающего поле "a" и ссылающегося на C5.
// Будет создан новый — С6
obj3.b = 9
Далее я постарался протестировать и определить разницу в производительности при работе с объектами в которых имена полей совпадают и в которых они отличаются.
Метод тестирования
Я провел три группы тестов:
- Static — имена свойств и их порядок не изменялись.
- Mixed — изменялась только половина свойств.
- Dynamic — все свойства и их порядок были уникальными для каждого объекта.
- Все тесты запускались на NodeJS версии 13.7.0.
- Каждый тест запускался в отдельном, новом потоке.
- Все ключи объектов имеют одинаковую длину, а все свойства одинаковое значение.
- Для измерения времени выполнения использовался Performance Timing API.
- В показателях могут быть незначительные колебания. Это вызвано разными фоновыми процессами исполняемыми на машине в тот момент.
- Код выглядит следующим образом. Ссылку на весь проект дам ниже.
const keys = getKeys(); performance.mark('start'); const obj = new createObject(keys); performance.mark('end'); performance.measure(`${length}`, 'start', 'end');
Результаты
Время на создание одного объекта
Первый, самый главный и простой тест: я взял цикл на 100 итераций. В каждой итерации я создавал новый объект и измерял время на его создание для каждой итерации.
График времени на создание одного объекта в зависимости от итерации
Как видите, во время первого, «холодного» запуска во всех группах создание объекта занимает практически идентичное время. Но уже на второй-третьей итерации скорость выполнения в группах static и mixed (там, где V8 может применить оптимизацию) по сравнению с группой dynamic значительно вырастает.
И это приводит к тому, что благодаря внутренней оптимизации V8 100-й объект из группы static создаётся в 7 раз быстрее.
Или, можно сказать по другому:
не соблюдение порядка объявления свойств в идентичных объектах может привести к семикратному замедлению вашего кода.
Результат не зависит от того как именно вы создаёте объект. Я испытал несколько разных способов и все они дали практически идентичные значения.
const obj = {}
for (let key of keys) obj[key] = 42
// Тоже самое что и первый вариант, но обёрнуто в функцию-конструктов
const obj = new createObject(keys)
const obj = Object.fromEntries(entries)
const obj = eval('({ ... })')
Суммарное время создания нескольких объектов
В первом тесте мы обнаружили, что объект из группы dynamic может создаваться на 30–40 микросекунд дольше. Но это только один объект. А в реальных приложениях их могут быть сотни или тысячи. Подсчитаем суммарные накладные расходы в разных масштабах. В следующем тесте я буду последовательно повторять первый, но замерять не время на создание одного объекта, а суммарное время на создание массива таких объектов.
График времени на создание массива объектов в зависимости от его размера
Как видите, в масштабах всего приложения простая внутренняя оптимизация V8 может дать ускорение в десятки раз.
Где это важно
Повсюду. В JavaScript объекты повсюду. Вот несколько жизненных примеров, где соблюдение порядка свойств даст прирост производительности:
При работе с Fetch
fetch(url1, {headers, method, body})
fetch(url2, {method, headers, body})
При работе с jQuery
$.css({ color, margin })
$.css({ margin, color })
При работе с Vue
Vue.component(name1, {template, data, computed})
Vue.component(name2, {data, computed, template})
При работе с паттерном Composite (компоновщик)
const createAlligator = () => ({...canEat(), ...canPoop()})
const createDog = () => ({...canPoop(), ...canEat()})
И в огромном множестве других мест.
Простое соблюдение порядка свойств в объектах одного типа сделает ваш код более производительным.
Ссылки
Код тестов и подробные результаты
О внутреннем устройстве V8 и оптимизации кода