Как V8 представляет значение
Каждое значение JS — это одно 64-битное машинное слово: либо тегированное малое целое (Smi), либо указатель на объект в куче. Это слово-тегированное-объединение и есть фундамент: первое слово HeapObject указывает на его Map, а один младший тег-бит решает
Вы пишете let x = 42, затем x = { id: 42 }, затем x = null. Для вас это три разных «вида» сущности. Для слота, хранящего x, всё это одно и то же: единственное 64-битное машинное слово. Движок должен уместить число, объект и «ничто» в одну коробку фиксированной ширины — и различать их при каждом чтении. Вся модель памяти V8 вытекает из того, как он это делает.
Одно слово, чтобы хранить любое значение
Переменная, элемент массива, слот свойства объекта, регистр внутри интерпретатора — всё это ровно одно машинное слово шириной, 64 бита на современном процессоре. Эта однородность не случайна; именно она делает движок простым и быстрым. Загрузка элемента массива — это одна и та же инструкция, будь элемент числом или объектом, потому что загружаемое — всегда одно слово. Движку никогда не нужно спрашивать «насколько велико это значение», прежде чем его передвигать.
Но значения JavaScript в принципе не одного размера. Малое целое умещается в слово тривиально. Объект, строка, замыкание — они произвольно велики. Поэтому V8 применяет классический системный трюк: слово — это tagged union (тегированное объединение). Оно хранит либо значение прямо в себе, либо указатель на то, где живёт настоящее значение, а тег говорит, какая трактовка применима.
Случаев ровно два:
- Smi («Small Integer», малое целое) — малое знаковое целое, хранимое прямо в слове. Никакого объекта в куче нет; биты слова и есть число. Чтение бесплатно.
- Указатель на HeapObject — слово содержит адрес объекта в куче. Настоящее значение (поля объекта, символы строки, 64 бита double) живёт по этому адресу. Чтение стоит разыменования.
Тег-бит: как движок их различает
Трюк в выравнивании. Каждый HeapObject размещается по чётному адресу (выровнен как минимум на 2 байта, на практике на 4 или 8), поэтому настоящий указатель всегда имеет младший бит, равный 0. V8 этим пользуется: он ставит младший бит указателя в 1, помечая «это слово — указатель», и оставляет младший бит 0, помечая «это слово — Smi». (Точную раскладку битов мы увидим в следующих двух уроках — здесь важно, что одного бита достаточно для различения.)
Поэтому каждый раз, глядя на слово-значение, движок первым делом делает один битовый тест по младшему биту:
// Концептуально любое обращение к значению начинается с этой ветки:
if ((word & 1) === 0) {
// Smi: целое закодировано в старших битах — используем напрямую
} else {
// HeapObject: сбрасываем тег и разыменовываем, чтобы дойти до объекта
}Эта ветка — одна инструкция процессора. Это входная плата за динамическую типизацию, и V8 два десятилетия делал её дешёвой и предсказуемой.
Все HeapObject начинаются с указателя на Map
Когда слово является указателем, на что оно указывает? Каждый HeapObject — объект, массив, строка, функция, упакованное число — начинает свою раскладку памяти с одного слова: указателя на свой Map. Map — это термин V8 для hidden class: он описывает, какого вида этот HeapObject и, для обычных объектов, раскладку его свойств (какие имена живут по каким смещениям).
Вот почему движок может разыменовать любой указатель на HeapObject и сразу узнать его тип и форму: ответ всегда по смещению 0.
Раскладка памяти HeapObject:
[ Map pointer ] <- смещение 0: «кто я и какова моя раскладка»
[ поле / данные ] <- следующее слово: зависит от вида
[ поле / данные ]
...Map — это стержень всей истории про производительность; hidden classes, inline caches и деопт держатся на нём. Map мы подробно разбираем в юните про hidden classes; здесь просто зафиксируйте структурный факт: значение — это слово; если его тег говорит «указатель», оно указывает на HeapObject; а первое слово HeapObject — это его Map.
Oddballs: true, false, null, undefined и hole
Так где же живут true, false, null и undefined? Они не Smi (это не целые) и не «объекты» в смысле JS. V8 представляет их как Oddballs — особые синглтонные HeapObject. В целом изоляте существует ровно один Oddball undefined, один null, один true, один false. Переменная, хранящая undefined, хранит указатель на этот единственный общий Oddball.
Есть и скрытый Oddball, который вы никогда не видите напрямую: hole (TheHole). V8 использует его как сторожевое значение, помечая пустой слот массива или неинициализированную привязку let (временная мёртвая зона — это буквально «в слоте всё ещё лежит hole»). Чтение hole и есть то, что вызывает ReferenceError при доступе в TDZ или проверку на дырку в разреженном массиве.
- Ширина любого слота-значения (64-бит)
- 1 слово = 8 байт
- Проверка тега Smi
- 1 битовый тест, 0 загрузок
- Доступ к HeapObject
- +1 разыменование
- Смещение указателя на Map
- 0 (первое слово)
- undefined / null / true / false
- синглтонные Oddballs
- Различных undefined на изолят
- 1 (общий)
Переменную переприсваивают из числа в объект. Почему слоту, хранящему её, не нужно менять размер?
Движок разыменовывает указатель на HeapObject. Что гарантированно лежит по смещению 0?
Расставьте шаги, которые делает движок, когда читает слово-значение, оказавшееся объектом, и ему нужно узнать его тип.
- 1 Проверить младший тег-бит слова-значения
- 2 Тег равен 1 — трактовать слово как указатель на HeapObject
- 3 Сбросить тег-бит, чтобы получить настоящий адрес
- 4 Разыменовать смещение 0, чтобы загрузить указатель на Map
- 5 Прочитать Map, чтобы узнать вид и раскладку объекта
▸Почему это работает
Почему смещать тегом именно указатель, а не Smi? Потому что арифметика Smi — это горячий случай, который движок больше всего хочет держать дешёвым. Оставив тег Smi равным 0, мы получаем целое в старших битах, и движок делает целочисленную математику с минимальным маскированием — без лишнего шага «снятия тега» на общем пути. Стоимость тега несёт указатель, поскольку его всё равно нужно маскировать перед разыменованием.
- 01Какие две трактовки возможны у слова-значения V8 и как движок выбирает между ними?
- 02Что гарантированно лежит по смещению 0 каждого HeapObject и почему это важно?
- 03Как V8 представляет true, false, null, undefined и hole?
V8 представляет каждое значение JavaScript как единственное 64-битное машинное слово, поэтому переменные, элементы массивов, слоты свойств и регистры интерпретатора однородно одной ширины в слово и перемещаются одной инструкцией независимо от типа. Это слово — tagged union с двумя случаями: Smi, где целое живёт прямо в битах с младшим тег-битом 0, и указатель на HeapObject, где слово — это выровненный адрес в куче с младшим тег-битом 1. Движок начинает любое обращение к значению с одного битового теста по младшему биту; только случай указателя платит за разыменование. Когда слово — указатель, оно достигает HeapObject, чьё первое слово, по смещению 0, всегда указатель на его Map (hidden class) — универсальный заголовок, сообщающий движку вид и раскладку объекта. Особые значения true, false, null, undefined и внутренний hole — синглтонные Oddball-HeapObject, общие на весь изолят. Это однородное тегированное слово — фундамент, на котором строятся следующие уроки: как кодируются Smi и double, как тег-биты на самом деле раскладываются и сжимаются, как устроена куча и как строки эксплуатируют ту же схему. Теперь, когда встретишь деопт, схему раскладки памяти в исходниках V8 или «указатель на HeapObject» в баг-репорте — ты знаешь, какое однобитовое решение лежит в основе.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.