Насколько важен порядок свойств в объектах 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


Далее я постарался протестировать и определить разницу в производительности при работе с объектами в которых имена полей совпадают и в которых они отличаются.

Метод тестирования


Я провел три группы тестов:

  1. Static — имена свойств и их порядок не изменялись.
  2. Mixed — изменялась только половина свойств.
  3. 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 итераций. В каждой итерации я создавал новый объект и измерял время на его создание для каждой итерации.

uc2vleemm85proxznmpak3ofldq.png
График времени на создание одного объекта в зависимости от итерации

Как видите, во время первого, «холодного» запуска во всех группах создание объекта занимает практически идентичное время. Но уже на второй-третьей итерации скорость выполнения в группах 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 микросекунд дольше. Но это только один объект. А в реальных приложениях их могут быть сотни или тысячи. Подсчитаем суммарные накладные расходы в разных масштабах. В следующем тесте я буду последовательно повторять первый, но замерять не время на создание одного объекта, а суммарное время на создание массива таких объектов.
sbm_ur9ofujdijqdr6gowffingq.png
График времени на создание массива объектов в зависимости от его размера

Как видите, в масштабах всего приложения простая внутренняя оптимизация 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 и оптимизации кода

© Habrahabr.ru