Быстрые свойства, медленные свойства и slack tracking
Три режима хранения: in-object-свойства (встроены в тело, один load), out-of-object fast properties (PropertyArray, +1 разыменование) и dictionary mode (хешмапа NameDictionary, ~50-100 циклов, включается delete'ом или избытком свойств).
Вы создаёте класс с тремя полями, но самые первые экземпляры, которые V8 аллоцирует, крупнее, чем нужно, — дополнены пустыми слотами. Затем, после нескольких сотен экземпляров, V8 тихо ужимает класс и освобождает дополнение. Это slack tracking, и если вы не знали о его существовании, вы неверно читали снимки кучи, снятые во время разогрева. Хранение свойств в V8 имеет три режима и один самонастраивающийся аллокатор, и то, в какой из них попадёт ваш объект, решает, будет ли чтение одной инструкцией или хеш-зондом.
Три режима хранения, три скорости
Почему место хранения так важно? Потому что разрыв в 50-100 раз между режимами — это именно тот обрыв, который виден как плоская полка на flame chart CPU, но исчезает при микроаллокациях. Его можно найти только если знаешь, что искать, ещё до профилирования.
DescriptorArray из урока 01 сообщает V8, где живёт поле. Возможных ответов три, в порядке убывания скорости:
- In-object-свойства. Хранятся встроенно в собственном блоке памяти объекта, сразу за заголовком. Чтение одного — это единственный load по постоянному смещению, самое быстрое из возможного. Число in-object-слотов фиксируется при создании Map.
- Out-of-object («normal») fast properties. Когда объект перерастает свои in-object-слоты, дальнейшие свойства проливаются в отдельный PropertyArray, на который объект указывает. Они всё ещё быстрые (фиксированное смещение, дружелюбны к IC), но стоят одного лишнего разыменования указателя: загрузить указатель PropertyArray, затем загрузить слот. DescriptorArray записывает индекс со смыслом «это поле в PropertyArray по индексу N».
- Dictionary mode (медленные свойства). Когда объект становится слишком динамичным — слишком много свойств или любой
delete— V8 полностью отказывается от раскладки с фиксированными смещениями и переводит объект в NameDictionary: пер-объектную хеш-таблицу из имени свойства в значение+attributes. Каждый доступ теперь — общий поиск по хешу, примерно 50–100 циклов против ~1 для in-object-load, и inline cache не может помочь.%HasFastProperties(obj)возвращаетfalse.
// d8 --allow-natives-syntax
const a = { p: 1, q: 2, r: 3 };
%HasFastProperties(a); // true — in-object, фиксированные смещения
delete a.q;
%HasFastProperties(a); // false — теперь NameDictionary, навсегда медленныйSlack tracking: V8 подгоняет размер ваших объектов
Вот самонастраивающаяся часть, которую обзор пропустил. Когда вы определяете конструктор или класс, V8 ещё не знает, со сколькими свойствами экземпляры окажутся в итоге — за this.x = ...; this.y = ... могут последовать другие присваивания в методах, вызванных позже. Аллоцировать ровно два in-object-слота заранее значило бы вынудить пролив в PropertyArray в момент появления третьего поля.
Поэтому V8 переаллоцирует: начальный Map конструктора резервирует лишние in-object-слоты (slack) — больше, чем объявляет тело конструктора. По мере создания и работы экземпляров V8 следит за максимальным числом in-object-свойств, которые они реально используют. После того как аллоцировано достаточно экземпляров (фиксированный порог по числу созданий), он завершает slack tracking: подрезает неиспользованные слоты, ужимает instance size в Map и финализирует Map. С этого момента каждый новый экземпляр аллоцируется в подрезанном, точном размере.
Последствия для сеньора:
- Экземпляры разогрева крупнее экземпляров установившегося состояния. Снимок кучи, снятый до завершения slack tracking, завышает размер на объект. Измеряйте после разогрева.
- Map не «финален» сразу. Код, зависящий от стабилизированной формы (и специализации TurboFan), получает её лишь после завершения slack tracking. Микробенчмарки, аллоцирующие горстку объектов, могут видеть другую форму, чем прод в масштабе.
- Добавление свойств вне конструктора после финализации уже не помещается в in-object-область и проливается в PropertyArray (или, если достаточно динамично, в dictionary mode).
Что толкает объект в dictionary mode
Dictionary mode — это обрыв. Вы падаете с него через:
delete obj.propна быстром объекте — самая частая причина. V8 не может оставить дырку в раскладке с фиксированными смещениями, поэтому конвертирует весь объект в NameDictionary. Это навсегда: объект никогда не поднимается обратно к fast properties.- Слишком много свойств, добавленных динамически (порог большой, порядка многих сотен до ~1000+, и зависит от версии), так что это редко настоящий виновник — виновник
delete. - Добавление свойств способом, который ломает дерево переходов — экстремальный churn форм может заставить V8 сдаться и переключиться в dictionary mode, чтобы перестать аллоцировать Map.
Заметьте, const-folding взаимодействует тут: когда поле const (по экземплярам присвоено лишь одно значение), V8 может хранить значение в Map (в дескрипторе), а не в объекте, так что чтения становятся «загрузить константу из Map» — но первое расходящееся присваивание теряет constness (урок 02), и значение переезжает в настоящее поле. Dictionary mode отказывается от всего этого.
- In-object-чтение
- 1 load (~1 цикл)
- Чтение PropertyArray
- 2 load'а (+1 разыменование)
- Чтение в dictionary mode
- ~50-100 циклов, хеш-зонд
- Что включает dictionary mode
- delete или экстремальная динамичность
- Выход из dictionary mode
- никакого — нужен свежий объект
- Slack tracking завершается после
- фиксированного порога по числу созданий
- Определить режим
- %HasFastProperties(obj) в d8
Чтение свойства компилируется в «загрузить указатель PropertyArray, затем загрузить слот 2 этого массива». В каком режиме хранения это свойство?
Снимок кучи после аллокации 10 экземпляров класса показывает, что каждый экземпляр крупнее, чем в снимке после аллокации 10000. Почему?
Расставьте по порядку жизненный цикл slack tracking для экземпляров конструктора.
- 1 Начальный Map резервирует лишние in-object-слоты сверх того, что объявляет тело конструктора
- 2 Экземпляры аллоцируются; V8 записывает максимум реально использованных in-object-свойств
- 3 Достигнут фиксированный порог по числу созданных экземпляров
- 4 V8 подрезает неиспользованные in-object-слоты и ужимает instance size
- 5 Map финализируется; каждый последующий экземпляр аллоцируется в подрезанном точном размере
▸Частая ошибка
Частая самопричинённая рана dictionary mode: использование обычного объекта как хешмапы с ключами от пользователя (cache[userId] = entry), а затем delete cache[userId] для вытеснения. Первый delete конвертирует cache в dictionary mode — что на самом деле корректно для настоящей мапы, но вы также теряете любую выгоду IC на нём и платите стоимость хеш-зонда на каждый доступ. Если объект концептуально словарь, используйте настоящий Map с самого начала: он создан для произвольных ключей и удалений, никогда не плодит hidden class и имеет предсказуемую производительность.
- 01Опишите три режима хранения свойств и стоимость чтения в каждом.
- 02Что такое slack tracking и почему размер объекта надо измерять после разогрева?
- 03Почему `delete` хуже присваивания null в терминах хранения?
V8 хранит именованные свойства объекта в одном из трёх режимов. In-object-свойства лежат встроенно в теле объекта и читаются единственным load’ом по постоянному смещению. Когда объект перерастает свои in-object-слоты, лишние свойства проливаются в out-of-object PropertyArray, стоя одного дополнительного разыменования указателя на чтение, но оставаясь с фиксированным смещением и дружелюбными к IC. Dictionary mode — это обрыв: запускаемый главным образом delete (и экстремальной динамичностью), он меняет фиксированную раскладку на пер-объектную хеш-таблицу NameDictionary, делая каждый доступ хеш-зондом ~50-100 циклов без помощи inline cache, и это навсегда — лишь свежий объект восстанавливает fast properties. Slack tracking — самонастраивающийся аллокатор V8 для быстрых объектов: начальный Map конструктора резервирует лишние in-object-слоты, V8 наблюдает, сколько экземпляры реально используют по мере создания, и после фиксированного порога по числу созданий подрезает slack, ужимает instance size и финализирует Map — так что память на объект устанавливается лишь после разогрева. Определяйте режим через %HasFastProperties(obj), предпочитайте null вместо delete и берите настоящий Map, когда ключи действительно динамичны. Теперь, когда увидишь false от %HasFastProperties на горячем объекте или снимок кучи, где размеры объектов уменьшаются после разогрева, ты знаешь, с какого слоя начинать разбор.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в184
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.