Поговорим про микро-оптимизацию. Про, казалось бы, самую безобидную вещь: обычный {}.[object Object] выглядит почти бесплатной абстракцией. Создал объект, напихал полей в любом порядке, удалил ненужное, передал дальше и забыл. Но, внезапно, одинаковые для тебя объекты могут оказаться разными для V8, порядок присваиваний влияет на машинный код, а “безобидный” delete оставляет после себя совсем не пустое место.Для 99% систем это не имеет значения. А вот если у тебя hot path, через который проходят десятки тысяч объектов в секунду, или ты пишешь библиотечный код, стоит знать, что V8 на самом деле делает с твоими объектами. И чего V8 там делает?
Поговорим про микро-оптимизацию. Про, казалось бы, самую безобидную вещь: обычный {}.
[object Object] выглядит почти бесплатной абстракцией. Создал объект, напихал полей в любом порядке, удалил ненужное, передал дальше и забыл. Но, внезапно, одинаковые для тебя объекты могут оказаться разными для V8, порядок присваиваний влияет на машинный код, а “безобидный” delete оставляет после себя совсем не пустое место.
Для 99% систем это не имеет значения. А вот если у тебя hot path, через который проходят десятки тысяч объектов в секунду, или ты пишешь библиотечный код, стоит знать, что V8 на самом деле делает с твоими объектами.
И чего он там с ними делает?
Когда ты создаёшь объект, V8 не выделяет «мешок ключей». Он строит для него hidden class (map внутри V8) структуру, которая описывает layout: какие у объекта поля, в каком порядке они лежат, какого типа, по каким офсетам читать. Сам объект это компактный массив значений, как struct в C. Hidden class хранится отдельно, и каждый объект держит на него ссылку.
Hidden classes связаны между собой через transitions и вместе образуют transition tree. Когда ты добавляешь поле к объекту, V8 идёт по исходящему переходу из текущего hidden class: если переход с таким полем уже есть переключается на существующий дочерний, если нет создаёт новый hidden class и новую ветку дерева.
Если несколько объектов идут по одним и тем же переходам в одинаковом порядке они разделяют один hidden class. Это даёт V8 базу для inline cache: «по этому офсету всегда лежит string, читаем напрямую, без проверок».
Когда hidden classes начинают расходиться (по порядку, по типам, по добавлению/удалению), inline cache переходит:
monomorphic (один shape — fast path),
polymorphic (до 4 shapes — V8 проверяет каждый, всё ещё быстро),
megamorphic (4+ — V8 сдаётся, идёт через generic lookup).
Стоимость растёт на каждом шаге.
Где ты теряешь?
Классический пример постепенная сборка vs литерал:
const a = {}
a.id = 1
a.name = 'jopa'
const b = { id: 1, name: 'jopa' }
Если порядок добавления полей совпадает, оба варианта в итоге попадают на один hidden class. У литерала есть бонус V8 запоминает финальный shape как boilerplate и при повторных вызовах аллоцирует объект сразу с готовым layout, без прохода по transition tree. Но в hot loop разница в установившемся режиме копеечная.
Хуже, если порядок полей в разных фабриках не совпадает:
function makeA(id, name) { return { id, name } }
function makeB(name, id) { return { name, id } }
Это уже два разных hidden class. Любой код, который читает obj.id через эти фабрики, видит два shape на одном call site и IC становится polymorphic. Само по себе ещё не больно, но если таких разных объектов не два, а шесть, то добро пожаловать в megamorphic.
Ещё кейс условное добавление полей:
const user = { id, name }
if (isAdmin) {
user.role = 'admin'
}
Часть объектов имеет shape {id, name}, часть — {id, name, role}. Уже polymorphic. Лучше класть role: null сразу и потом присвоить значение при необходимости.
delete — отдельная история
delete user.name
Вот тут V8 уже не прощает. delete переводит объект в dictionary mode (он же slow mode, normalized properties). Это hashmap вместо фиксированного layout. Ни inline cache, ни офсетов, каждое чтение поля идёт через hash lookup в C++ runtime. И обратно V8 объект уже не вернёт, даже если форма стабилизировалась.
Вроде как память освободил. А по факту превратил объект в Map без типизации.
Если поле больше не нужно лучше присвой null или undefined. Shape сохранится, движок не потеряет в оптимизации.
А что по цифрам?
Стенд: Node 24, миллион объектов с тремя полями, в hot loop читаем obj.id, меряем ns/op. По три прогона, чтобы JIT успел устаканиться.
literal (same shape): 3.99 нс
stepwise (same order): 4.08 нс
poly IC (2 shape): 4.65 нс
megamorphic (6 shapes): 8.51 нс
dict mode (after delete): 56.12 нс
Что видно:
literal vs stepwise — разницы практически нет. V8 одинаково хорошо справляется с обоими.
polymorphic IC (2 shape) — ~15% оверхед. V8 спокойно тянет до 4 shape на одном call site.
megamorphic (6 shape) — x2+ к чтению. Уже серьёзно.
delete (dictionary mode) — x14 медленнее. Больно :)
На 100k RPS × 50 чтений полей на запрос с dict-mode объектами вместо нормальных — потеря ~260 мс CPU/с.
А что было на Node 22?
Для наглядности — те же сценарии на Node 22:
literal: 11.0 → 4.0 нс (x2.8)
stepwise: 11.1 → 4.1 нс (x2.7)
poly (2 shape): 10.3 → 4.7 нс (x2.2)
megamorphic: 14.1 → 8.5 нс (x1.7)
dict mode: 56.8 → 56.1 нс (=)
Что интересно:
Fast path между релизами стал ~x2.5 быстрее. TurboFan и Maglev продолжают тюниться, монохромное чтение поля на Node 24 — 4 нс против 11 на Node 22.
Dict mode — константа. 56 нс на обеих версиях. Это C++ hashmap lookup, JIT там не играет, оптимизировать нечего.
Относительная разница fast/slow растёт. На Node 22 delete был x5 медленнее literal’а, на Node 24 — уже x14. Fast path уходит вперёд, slow path стоит.
Вывод: с каждым новым V8 сидеть в dictionary mode или megamorphic IC становится всё дороже относительно “правильного” кода.
А что делать то?
В hot path — литералы, со всеми полями сразу. Не знаешь значение полож null.
Поля во всех фабриках — в одном порядке. Делаешь { id, name }, делай везде { id, name }.
delete — не нужон. Только obj.field = null (или undefined).
Если объект сложный и часто создаётся — класс или фабрика. Один shape гарантированно.
Не стоит превращать один call site в универсальную точку на все случаи жизни.: четыре shape потолок V8, дальше generic.
Что в итоге?
Объект в JS не «просто словарь». Это контракт с V8: ты обещаешь стабильный shape, V8 обещает быструю работу через hidden classes и inline cache. Нарушаешь — съезжаешь в polymorphic, потом megamorphic, потом dictionary mode. С каждым шагом дороже.
В обычном CRUD это не заметно. На hot path — это десятки процентов CPU и заметные хвосты по p99.
Date.now() тратил CPU на syscall, parseFloat — на парсинг, плохой shape — на cache miss. А потом говорят: “ой да ну какой Node, он же медленный, давайте напишем на Go и пойдем за ванильным лате на растительном”
Но shape, на самом деле, это только половина истории. Даже при том же hidden class смена типа значения в поле способна инвалидировать уже оптимизированный код. В следующей статье расскажу, что V8 на самом деле хранит за number, string и null, при чём тут Smi, Double, HeapObject и Tagged и почему совет «положи null заранее» работает не для каждого поля.
Node быстрый. Просто не мешай ему.
Что почитать?
V8 internals: hidden classes & inline caches — Mathias Bynens, must-read
Fast properties in V8 — официальный блог V8
Elements kinds in V8 — про массивы и их shapes
PiterJS#91 — запись доклада
zerodeps-tech/object-shapes-bench — бенчмарки для статьи
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | «Зачем Javasript-у DI контейнер?» | 0 | 8.78 | 21-07-2026 |
| 2 | Реальная и мнимая многопоточность в JS | 0 | 5.83 | 30-07-2026 |
| 3 | С++: Пиши, сокращай, оптимизируй | 0 | 10.41 | 23-07-2026 |
| 4 | Модель исполнения JavaScript по спецификации ECMAScript: call stack, контексты, окружения и замыкания | 0 | 6.66 | 28-07-2026 |
| 5 | Дайджест JS/TS: новинки ES2026, гонка рантаймов и EAP | 0 | 12.18 | 29-06-2026 |
| 6 | Давайте заглянем в этот самый вайб-код | 0 | 8.44 | 20-03-2026 |
| 7 | AAA Движок для GTA San Andreas в браузере и переписывание ThreeJS | 0 | 6.18 | 04-08-2026 |
| 8 | Худший язык программирования всех времён /s | -2 | 6 | 01-07-2026 |
| 9 | Тап по тысяче точек за O(log n): QuadTree и сферическая геометрия в гео-соцсети | 0 | 7 | 28-06-2026 |
| 10 | [Перевод] Структуры данных на практике. Глава 16: Фильтры Блума и вероятностные структуры данных | 0 | 7 | 28-06-2026 |