В прошлой статье про объекты и скорость доступа к известному полю я рассказал про hidden classes, inline cache и форму объекта. Практический совет там был простой: если поле опциональное, создай его сразу и положи null. Тогда экземпляры сохранят одинаковую форму.

Совет рабочий, но неполный. Форма объекта у V8 может остаться прежней, а вот внутренний контракт хранения поля может измениться. Если горячая функция успела прогреться под старый контракт, V8 пометит её на деоптимизацию.

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

Map знает не только форму

У каждого объекта в V8 есть Map, он же hidden class. Map описывает набор полей, их порядок и смещения. В дескрипторе поля есть ещё и представление, то есть способ, которым V8 планирует хранить значение.

Упрощённо нужная нам часть выглядит так:

Smi        маленькое целое, закодированное прямо в tagged-слоте
Double     число с плавающей точкой
HeapObject ссылка на объект в куче: строка, объект, `null`, `undefined` и так далее
Tagged     широкое представление, которое принимает и Smi, и ссылки

Это контракт хранения поля. В самом V8 отдельно существует ещё и field type, но для перехода из number в string достаточно представления.

Когда V8 много раз видит { x: 42 }, поле x получает представление Smi. Оптимизатор по известной форме читает поле как маленькое целое и строит код под это допущение.

Потом кто-то делает так:

obj.x = 'jopa'

Строка в Smi не помещается. V8 расширяет представление до Tagged, чтобы поле могло хранить и целые числа, и ссылки на объекты в куче.

Ключевой момент: это не обязательно создаёт новую форму (shape, Map). Для некоторых расширений V8 обновляет дескриптор на месте, и адрес Map остаётся тем же. Но вот оптимизированный код зависел не только от адреса Map, а ещё и от представления. Старому коду больше нельзя доверять.

Вот сам деопт

Минимальный пример проверен на Node.js 24.17.0 с V8 13.6.233.17-node.49:

function readX(object) {
  return object.x
}

const a = { x: 1 }
const b = { x: 2 }

// До прогрева помечаем поля как изменяемые (kConst/kMutable в V8)
a.x = 3
b.x = 4

for (let i = 0; i < 100_000; i++) {
  readX(i & 1 ? a : b)
}

// --allow-natives-syntax
%OptimizeFunctionOnNextCall(readX)
readX(a)

a.x = 'jopa'

Запускаем:

node --allow-natives-syntax --trace-deopt demo.js

В трейсе будет строка такого вида:

[marking dependent code ... (readX) for deoptimization,
 reason: dependent field representation changed]

Одна запись строки пометила уже оптимизированную readX на деоптимизацию. На этой версии V8 %HaveSameMap(a, b) остаётся истинным и после присваивания строки: форма та же, Map тот же, а вот зависимый код всё равно отправлен на деоптимизацию.

%OptimizeFunctionOnNextCall и %HaveSameMap являются внутренними средствами диагностики V8. В прод тащить их, разумеется, не нужно.

Где здесь подвох с null

Вернёмся к совету «создай поле заранее и положи null»:

const user = { id, name, role: null }

if (isAdmin) {
  user.role = 'admin'
}

Для формы объекта совет по-прежнему полезен. Поле role есть у всех экземпляров с самого начала. Кроме того, и null, и строка относятся к HeapObject, поэтому представление при такой записи расширять не нужно.

С числом история иная:

const stats = { score: null } // HeapObject
stats.score = 42              // теперь поле должно принять ещё и Smi, получаем Tagged

Если код успел оптимизироваться между этими двумя состояниями, расширение поля может инвалидировать зависимый оптимизированный код.

Для целочисленного поля лучше сразу задать числовое значение:

const stats = { score: 0 }
stats.score = 42

С undefined та же история, что с null: это не числовая заглушка. А если поле хранит дробные числа, одного 0 тоже недостаточно для идеальной стабильности. Переход от Smi к Double может потребовать ещё одно расширение и привести к деоптимизации.

Но не стоит гнаться за этим и превращать в абсурд. Если null является нормальным и честным состоянием бизнес-модели, оставь null. Все конечно же зависит от контекста. Семантика бизнесовой модели важнее потенциальной деоптимизации на прогреве. А вот для горячей структуры идеальный вариант другой: присвоить реальное значение до того, как код станет горячим.

Расширение происходит не на каждом присваивании

Тут можно сделать неправильный вывод, вроде как каждое переключение 42 → 'jopa' → 42 снова деоптимизирует функцию из-за представления.

Нет. Представление расширяется до Tagged один раз и самостоятельно обратно до Smi уже не скукожится. После этого и число, и строка помещаются в установленный тип представления. Повторной смены предсталвения на каждом переключении нет.

Деопты всё ещё возможны, если горячий код специализировался на фактических значениях. Например, выражение object.x + 1 сначала выполняет числовое сложение, а со строкой начинает конкатенацию.

Это уже другая история: у операции своя обратная связь по типам, а у конкатенации ещё и свои аллокации. Без --trace-deopt эти эффекты не увидеть. В трейсе будет видно, что у смены представления, обратной связи операции и конкатенации разные причины.

Поэтому я не буду продавать красивое «переключение типов замедляет цикл ровно в два раза». Такой микробенч легко измеряет одновременно чтение поля, смену представления, арифметическую специализацию, конкатенацию и повторную оптимизацию. Цифра получится эффектной, но инженерного смысла в ней будет примерно ни фи га.

Тут в целом достаточно трейса: первое расширение представления способно отправить на деоптимизацию весь оптимизированный код, который от него зависит. Цена конкретного деопта зависит от функции, нагрузки, версии V8 и того, сколько зависимого кода придётся пересобрать.

Что делать

В большинстве случаев ничего. Это не призыв писать TypeScript ради V8 и не повод заменять честный null на ноль. Это просто ещё один контракт, который ты заключаешь с рантаймом на горячем пути.

  • Создавай горячие объекты с полным и стабильным набором полей.

  • Для целочисленного поля используй числовое начальное значение, а не null или undefined.

  • Присваивай реальное значение до прогрева, особенно если поле может хранить дробные числа.

  • Не смешивай число и строку в одном поле без причины.

  • Проверяй подозрения через --trace-deopt на той версии Node.js, которая работает у тебя в проде.

Что в итоге

Одинаковая форма объекта ещё не гарантирует, что оптимизированный код останется валидным. V8 учитывает представление.

Переход number → string опасен не потому, что строка сама по себе медленная. Проблема возникает в момент, когда ломается контракт уже скомпилированной функции. V8 расширяет поле, помечает зависимый код на деоптимизацию и затем работает с более широким представлением.

null не хороший и не плохой. Для ссылочного поля это нормальная заглушка. Для числового поля это уже другой внутренний контракт.

JavaScript разрешает менять типы как угодно. V8 тоже не против. Но без причины лучше не меняй.

Что почитать

Комментарии (12)


  1. Aquahawk
    15.08.2026 13:00

    Лет 8-10 назад кто-то на Holy JS рассказывал случай. Выкатили новую версию игрового сервера на NodeJS, и буквально к вечеру всё стало ужасно тормозить, CPU забился доверху. Стали разбираться. Оказалось в этом релизе выкатили функционал премиум подписки. На объекте юзера звели поле isPremium, которое при конструировании игрока не инициализировалось, а если он покупал премиум, то поле накидывалось со значением true. В итоге небольшой процент юзеров купивший премиум имел другой скрытй класс и при попадании я разные функции то тут то там деоптимизировал их. Добавили в конструктор юзера явную инициализацию поля и вся производительность вернулась.


  1. jarick
    15.08.2026 13:00

    Можно провокационный вопрос оффтоп. Насколько, на взгляд автора, уместно спрашивать на собеседовании frontend-разработчика вопросы, которые были описаны в его статьях? Есть опасение получить негативный фидбек о токсичности, если начать копать так глубоко и спрашивать о V8-оптимизациях у соискателей. Есть ли у кого-то тут подобный опыт?


    1. VaskaDeGama Автор
      15.08.2026 13:00

      Ох, интересный вопрос) Как по мне, это и не каждому бэку нужно. Ну и от позиции зависит. Условный джун, тоже наверное нет, ему бы просто все приколы языка познать.

      От мидла+ наверное хорошо бы, чтобы разработчик, хотя бы интересовался, как там язык под капотом работает.

      На собеседовании, скорее нет, чем да. Можно затронуть эту тему, но не в разрезе контрольного вопроса, а так, мимоходом. Интересовался, изучал, что то трогал кандидат в этой стороне.

      А так же от проекта, команды зависит. Если команда пишет фронт на реакте для crm, тут оно точно не надо. А вот если команда пилит условную молотилку цифр и перекладывалку джисонов на кучу запросов в секунду или мало запросов, но тяжелых, тут дело другое.

      В общем, зависит от обстоятельств и настроения обоих участников допроса.


    1. Aquahawk
      15.08.2026 13:00

      В геймдеве у синьёров я спрашиваю обязательно. Хоть какое-то понятие об этом должно быть. И всех, начиная с джуна спрашиваю: "так случилось, что то, что вы сделали тормозит, в любом смысле этого слова, ваши действия, как вы будете искать проблему?" очень многое говорит о том задумывается ли человек о производительности вообще, как широки и как глубоки его понятия об этом. Концепцию О нотации обязательно даже для джуна в геймдеве.


      1. OlegZH
        15.08.2026 13:00

         "так случилось, что то, что вы сделали тормозит, в любом смысле этого слова, ваши действия, как вы будете искать проблему?"

        В естественном и убывающем порядке важности:

        1. Выбрал не тот алгоритм/структуру данных; дефекты реализации.

        2. Незнакомая мне особенность языка.

        3. Неправильная/неэффективная технологическая платформа/технология/язык программирования.

        Возможно, упустил что-то ещё.


        1. Aquahawk
          15.08.2026 13:00

          Так, начало есть, а теперь ваши действия, как локализовать проблему?


          1. OlegZH
            15.08.2026 13:00

            [ Ну, я даже и не джун... ;-( ]

            Во-первых, можно поменять алгоритм. Если, например, речь идёт о сортировке элементов некоторого массива, то на разных данных более быстрыми будут разные алгоритмы. Различие в данных относится и к их размеру (тут в дело вступают константы оценок сложности), и к их содержанию (бывают, так сказать, частично упорядоченные массивы). В случае, если речь идёт, например, о построении матричного профиля временного ряда (это такая функция, которая сопоставляет каждому фрагменту временного ряда расстояние до заданного образца из другого временного ряда: Вы проводите скользящее окно фиксированной ширины/длины вдоль временного ряда и получаете новый временной ряд, который и называется матричным профилем), то его непосредственное вычисление часто оказывается невозможным (слишком много сравнений!), зато имеются техники, позволяющие существенным образом сократить количество сравнений (и вычислять матричный профиль за секунды и минуты против часов и дней).

            Во-вторых, можно "прошерстить" сам алгоритм, и проверить правильно его реализации. Можно попытаться переписать его, возможно, выбрав другие структуры данных (с учётом времени доступа и времени обновления структуры). Ещё бывает очень дорогим вызов процедур/функций. Плюс оптимизация компилятора. И не забудем про кеширование в оперативной памяти. А многопоточность? Вот, бывает так, что процесс хочет записать что-то в память, а память ещё не готова принять данные?

            В-третьих, (а, точнее, во-первых!), надо всё тщательно измерить. Любое оценочное суждение должно быть подкреплено числом. Запускать надо на разных конфигурациях/машинах. Надо точно знать, точно ли, что это колесо доедет до Москвы, а до Казани не до едет.


    1. TheHost
      15.08.2026 13:00

      По сути тут не про V8 даже. Это буквально база компьютерных наук, того как компьютер представляет данные. Взять тот же C, C++ и сразу станет понятно чем хорошо/плохо смена типа в рантайме.
      Так что спросить можно у любого программиста, но учитывая, что сейчас 9 из 10 не могут уверенно назвать разницу между var, let, const - не обязательно)))


      1. jarick
        15.08.2026 13:00

        К сожалению, вопрос про varlet и const задаётся, как правило, на любом собеседовании, которыми сейчас наполнены YouTube и тематические группы в Telegram. В 2026 году количество людей, которые научились заучивать вопросы на скринингах и подготовлены менторами, увы, превысило возможности HR-отдела по обработке резюме. Тем более что графа «опыт работы» абсолютно девальвировалась и превратилась в фарс. Фактически остаётся только ужесточать требования и спрашивать базу компьютерных наук, но вызывает серьёзные опасения, что на фронтенде такие вопросы вызовут крайне негативную реакцию.


        1. TheHost
          15.08.2026 13:00

          ну я собешу людей, я без шуток говорю, 9 из 10 не может ответить. Даже разницу в области видимости. И это люди которые якобы 5+ лет опыта)

          что на фронтенде такие вопросы вызовут крайне негативную реакцию.

          Рыночек сейчас не тот. Но вполне можно упустить нормального, если он просто, ну забыл нюансы какие-то, которые в целом и не нужны верстая формочки


          1. jarick
            15.08.2026 13:00

            Просто с задачей верстать формочки уже и железный друг научился неплохо справляться. Как правило, фронтенд в 2026 году — это что-то из: BD UI, monorepo, FSD, свой UI kit, frontops, выстраивания пирамиды тестирования, storybook, BFF, Rust и прочие сложные вещи.


            1. TheHost
              15.08.2026 13:00

              Да, даешь ему фигму и все)) Но местами еще косячит)