Четыре яруса и как работает разогрев
V8 исполняет код на четырёх ярусах — Ignition, Sparkplug, Maglev, TurboFan — обменивая стоимость компиляции на скорость исполнения. Как счётчики вызовов и обратные дуги циклов запускают повышение яруса и почему верхние ярусы компилируются параллельно на фоновых потоках.
Вы запускаете функцию 10 000 раз в бенчмарке и видите, как время одного вызова падает тремя отчётливыми ступенями — небольшой провал, затем больший, затем финальный обрыв в нативную скорость. Эти ступени — не шум. Это ваша функция взбирается по четырём разным компиляторам, каждый из которых готов потратить больше времени на производство более быстрого кода, чем нижестоящий. Понимать, на какой вы ступени и что запускает следующую, — это разница между бенчмарком, который разогревается, и продакшен-путём, который так и не разогрелся.
Четыре яруса, одна кривая «стоимость против скорости»
Обзорный урок четырёхъярусный JIT-конвейер V8 набросал лестницу; этот юнит открывает каждую ступень. Начнём с формы компромисса. Каждый ярус сидит в точке на одной оси: сколько времени уходит на то, чтобы произвести код, против того, как быстро этот код исполняется. Дешевле скомпилировать — медленнее исполнять; быстрее исполнять — дороже компилировать. V8 поставляет четыре яруса, потому что ни одна точка на этой кривой не подходит всему коду.
Ignition — это интерпретатор. Парсер передаёт ему AST; Ignition один раз понижает его до компактного регистрового байт-кода, а затем исполняет этот байт-код на виртуальной машине. Здесь вообще нет генерации машинного кода — исполнение это цикл диспетчеризации, который читает байт-код, прыгает в обработчик, исполняет его и сдвигается дальше. Стоимость входа фактически нулевая (вы уже заплатили за производство байт-кода), и это единственный ярус, который записывает обратную связь о типах, на которую опираются верхние ярусы. На операцию он примерно на порядок медленнее оптимизированного нативного кода, потому что каждый шаг платит за накладные расходы диспетчеризации интерпретатора — поиск в таблице и непрямой переход.
Sparkplug — это базовый (baseline) JIT, появившийся в V8 9.1 (2021). Это ступень, которую чаще всего понимают неправильно, поэтому будем точны: Sparkplug не делает оптимизаций. Он не строит промежуточного представления (IR), не делает инлайнинга, escape-анализа, специализации по типам. Он один раз обходит уже существующий байт-код и выпускает почти 1:1 шаблон машинных инструкций — на каждый байт-код фиксированный небольшой блок нативного кода плюс изредка вызов обратно в рантайм. Что он убирает — это ровно накладные расходы диспетчеризации интерпретатора: больше нет поиска в таблице и непрямого перехода на операцию, только прямолинейный нативный код. Одно это даёт примерно 1.5–2× над Ignition, а компилируется за микросекунды (около 1 мс на килобайт байт-кода). Поскольку компиляция настолько дёшева, код Sparkplug можно производить лениво — при первой «горячести» — или жадно.
Maglev — это среднеярусный оптимизирующий компилятор, появившийся в 2023. Это настоящий оптимизатор — он строит IR в форме SSA и использует записанную обратную связь о типах, чтобы специализировать загрузки свойств и арифметику, — но лёгкий. Он использует быстрый аллокатор регистров методом линейного сканирования и пропускает самые тяжёлые проходы TurboFan (escape-анализ, полиморфный инлайнинг, итеративное сужение типов). В результате он компилируется примерно в 10 раз быстрее TurboFan, достигая около 50–70% качества его кода: верный инструмент для «горячих, но не самых горячих» функций, где задержка компиляции TurboFan навредила бы сильнее, чем помогла бы его лишняя скорость.
TurboFan — это полный оптимизирующий компилятор: IR в виде «моря узлов» (sea-of-nodes), агрессивный инлайнинг, escape-анализ и всё прочее (урок 03 этого юнита открывает его). Он производит самый быстрый код, какой может сделать V8, ценой компиляции в десятки-сотни миллисекунд на функцию. Платить за это стоит только для кода, который и очень горячий, и стабилен по типам.
Как разогрев на самом деле запускает повышение яруса
Функция поднимается не из-за таймера по настенным часам. Она поднимается, потому что два счётчика пересекают бюджеты. Первый — счётчик вызовов (invocation count): сколько раз в функцию входили. Второй — счётчик обратных дуг (back-edge count): сколько раз исполнение прыгало назад к началу цикла внутри функции. V8 держит бюджет на функцию («бюджет прерывания» / бюджет обратной связи); каждый вызов и каждая обратная дуга цикла уменьшают его, и когда он достигает нуля, рантайм запускает проверку повышения яруса. Вот почему и горячие функции (часто вызываемые), и горячие циклы (часто повторяемые, даже внутри функции, вызванной однажды) могут запросить более высокий ярус — счётчик обратных дуг это то, что позволяет одному долго работающему циклу оптимизироваться, ни разу не вернувшись.
// Вызывается много раз -> счётчик вызовов запускает повышение яруса.
function distance(a, b) {
const dx = a.x - b.x, dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
for (let i = 0; i < 1_000_000; i++) distance(p, q);
// Вызывается однажды, но обратные дуги цикла запускают повышение яруса ЭТОГО фрейма.
function sumTo(n) {
let total = 0;
for (let i = 0; i < n; i++) total += i; // каждая итерация = одна обратная дуга
return total;
}
sumTo(100_000_000);Пороги динамические и настраиваются от релиза к релизу — грубо говоря, Sparkplug включается после низких сотен вызовов, Maglev после низких тысяч, TurboFan после десятков тысяч, — но никогда не зашивайте число; относитесь к ним как к порядку, а не к константам.
- Стоимость компиляции Ignition
- 0 (уже байт-код)
- Скорость компиляции Sparkplug
- ~1 мс / кБ байт-кода
- Ускорение Sparkplug над Ignition
- ~1.5-2x
- Время компиляции Maglev
- ~10 мс / функцию
- Качество кода Maglev против TurboFan
- ~50-70%
- Время компиляции TurboFan
- десятки-сотни мс
- Sparkplug появился
- V8 9.1 (2021)
- Maglev появился
- V8 11.x (2023)
Параллельная компиляция: интерпретатор никогда не блокируется
Если бы компиляция была синхронной, вы получали бы 100-мс заморозку каждый раз, когда функция доходит до TurboFan. Она не синхронная. Повышение яруса не останавливает вашу программу. Когда бюджет срабатывает, V8 ставит задачу компиляции в очередь, и фоновый поток запускает Maglev или TurboFan, пока главный поток продолжает исполнять функцию на её текущем ярусе. Когда оптимизированный код готов, V8 подменяет его при следующем входе (или прямо посреди цикла, через замену на стеке — урок 06). Вот почему вы никогда не видите 100-мс заморозку, когда функция повышается до TurboFan: заморозка случилась бы, если бы компиляция была синхронной, но она не такая. Цена проявляется вместо этого как фоновая нагрузка на CPU и короткое окно, где функция исполняется на старой скорости, хотя уже «запланирована» на новый ярус.
За всем этим можно наблюдать. --trace-opt логирует каждое решение об оптимизации (какая функция, какой ярус, почему); --no-opt целиком отключает оптимизирующие ярусы, чтобы сравнить с чистыми Ignition плюс Sparkplug. В Node: node --trace-opt --trace-deopt app.js. В d8 добавьте --allow-natives-syntax, чтобы вызвать %OptimizeFunctionOnNextCall(fn) и форсировать повышение яруса немедленно, а не разогревать вручную.
Что Sparkplug на самом деле делает с вашим байт-кодом?
Функция вызвана ровно один раз, но содержит цикл на 50 миллионов итераций. Что позволяет V8 её оптимизировать?
Расставьте ярусы V8 от самого дешёвого в производстве (исполняется первым) до самого оптимизированного (только самый горячий код).
- 1 Ignition — интерпретирует байт-код, записывает обратную связь о типах
- 2 Sparkplug — базовый JIT, машинный шаблон 1:1
- 3 Maglev — среднеярусный SSA-оптимизатор, лёгкая специализация
- 4 TurboFan — полный оптимизатор, самый быстрый код
▸Почему это работает
Зачем добавлять Sparkplug и Maglev между Ignition и TurboFan, а не просто настраивать пороги? Потому что разрыв был и широким, и двухмодальным. Sparkplug закрывает разрыв по накладным расходам диспетчеризации почти бесплатно, так что тёплый-но-не-горячий код перестаёт платить интерпретаторный налог во время загрузки страницы. Maglev закрывает разрыв по специализации для средне-горячего кода без задержки компиляции TurboFan, что важно для бюджетов времени кадра — анимация на 60 fps, которая вызывала бы TurboFan на каждый новоиспечённо горячий хелпер, прожгла бы свои 16 мс кадра на конкуренцию за фоновую компиляцию. Две дешёвые промежуточные ступени лучше одного большого прыжка.
- 01Назовите четыре яруса V8 по порядку и единственное определяющее свойство каждого.
- 02Какие два рантайм-счётчика запускают повышение яруса и зачем нужны оба?
- 03Почему повышение яруса до TurboFan не замораживает главный поток на 100 мс и как наблюдать весь процесс?
V8 исполняет JavaScript на четырёх ярусах, расположенных вдоль одной кривой «стоимость компиляции против скорости исполнения». Ignition интерпретирует регистровый байт-код с нулевой стоимостью компиляции и является единственным ярусом, записывающим обратную связь о типах, нужную оптимизаторам. Sparkplug — это базовый JIT, выпускающий почти 1:1 машинный шаблон за микросекунды, удаляя накладные расходы диспетчеризации интерпретатора ради выигрыша в 1.5-2x, — но он не делает никаких оптимизаций: ни IR, ни инлайнинга, ни специализации. Maglev — это лёгкий SSA-оптимизатор, который по обратной связи специализирует арифметику и доступ к свойствам, компилируясь ~10x быстрее TurboFan при ~50-70% его качества кода. TurboFan — полный оптимизатор, самый быстрый и самый дорогой. Функции поднимаются по лестнице, когда их счётчик вызовов и счётчик обратных дуг цикла срабатывают по бюджету на функцию — счётчик обратных дуг это то, что позволяет оптимизировать однажды вызванную функцию с длинным циклом. Компиляция идёт на фоновых потоках, так что главный поток продолжает исполнять текущий ярус и никогда не блокируется; новый код подменяется при следующем входе. Наблюдайте за всем этим через —trace-opt, —no-opt и интринсики natives-syntax в d8. Теперь, когда вы увидите три ступени в бенчмарке, вы знаете, какую границу яруса каждая означает — и какой счётчик смотреть, чтобы ваш продакшен-путь её пересёк.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.