В этой серии статей мы пройдемся по каждому этапу работы V8: лексическому и синтаксическому анализу, построению AST, интерпретации и оптимизациям. Затронем Ignition, Sparkplug, Maglev и Turbofan; разберемся с hidden classes, байт-кодом, и много чем еще. Углубимся во все этапы достаточно, чтобы понимать, за что отвечает каждый из них.
Основные среды выполнения JavaScript на текущий момент это Node.js (с V8 по умолчанию), Electron на базе Chromium, который под капотом также имеет V8, и браузеры:
Google Chrome (с движком V8).
Mozilla Firefox (с движком SpiderMonkey).
Microsoft Edge (до 2020 года с Chakra, сейчас на Chromium с V8).
Safari (с движком JavaScriptCore в WebKit).
В разных средах разное окружение. В данном цикле мы будем рассматривать Google Chrome с его V8.
Внимание! Браузерные движки, как и языки программирования, постоянно развиваются. Описанное в статье соответствует состоянию V8 и спецификации ECMAScript на 2025 год. Некоторые детали реализации могут измениться в будущем.

Лексический анализ
На первом шаге любая JavaScript-программа подвергается лексическому анализу, который выполняет компонент V8 под названием Scanner.
Его задача — разбить исходный код на поток отдельных токенов в соответствии с лексической грамматикой, описанной в спецификации ECMAScript.
Например, простая строка const hello = 'World!';
внутри V8 превратится в нижеприведенный набор токенов:

Здесь type
— это определенный тип из EcmaScript, value
— подпадающая под него строка, start
и end
— позиции в строке.
В большинстве случаев сканеру достаточно проанализировать следующие 1-2 символа, чтобы понять, с каким токеном он имеет дело. Например, рассмотрим оператор сравнения:
==
Сначала сканер возьмет первый символ =
. Но в JavaScript существует ещё несколько операторов, начинающихся со знака =
, и поэтому он заглянет (lookahead) ещё дальше и проанализирует второй и третий символы. Благодаря «заглядыванию вперед» он поймет, что перед ним Punctuator ==
.
Максимальный lookahead в сканере равен 4 символам, и этого достаточно, чтобы разобрать даже самые длинные языковые конструкции, например, >>>= —
токен, различимый от >>
, >>>
и >>=
только при анализе четырёх символов вперёд.
Помимо токенизации, сканер также отмечает потенциальные места вставки ;
(только отмечает, не вставляя). Эта мета-информация нужна будет далее, во время парсинга.
Довольно популярный пример:
function getValue() {
return
{
value: 42
}
}
getValue() // { value: 42 } ?
С точки зрения работы движка сканнер пройдет по данному коду и отметит строку с return
как потенциальное место для вставки точки с запятой. На следующем этапе, во время парсинга, ;
действительно подставится, создавая «неожиданное» поведение:
function getValue() {
return;
{
value: 42
}
}
getValue() // undefined
Этот механизм называется ASI (Automatic Semicolon Insertion), и важно учитывать его работу, чтобы избежать неожиданных результатов при выполнении кода.
Стоит отметить, что даже невалидные программы, вроде...
const = = ;
...ещё не выдадут ошибку на данном этапе. Сканер спокойно разобьет нашу программу на токены и отдаст дальше, ведь проверка синтаксической корректности не его зона ответственности.
Синтаксический анализ
После лексического анализа токены попадают в Parser. На этом этапе стрим токенов подвергается синтаксическому анализу, согласно тому, как V8 реализует спецификацию EcmaScript.
Синтаксический анализатор в V8 состоит из двух компонентов — PreParser и Parser.
PreParser
Согласно спецификации EcmaScript весь JavaScript код перед его запуском должен быть синтаксически проанализирован. Это означает, что все синтаксические ошибки должны быть выявлены ещё до начала исполнения.
Но совершать полный синтаксический анализ перед запуском — слишком долго, особенно в случае с большими программами, а скорость это одна из важнейших вещей для пользователя.
Для решения этой проблемы инженерами Google был создан PreParser, значительно ускоряющий запуск кода интерпретатором. Весь не нужный в момент запуска программы код обрабатывается именно им.
PreParser в V8 выполняет поверхностный синтаксический разбор всего верхнего уровня JavaScript-кода. Однако только в функциях он заходит внутрь и анализирует тело: параметры,
return
,await
,arguments
,super
и прочее.
Все остальные конструкции — объявления переменных, условия, циклы, вызовы функций — препарсятся поверхностно. PreParser лишь подтверждает, что синтаксис валиден, но не анализирует внутренние выражения и не строит структуру.
V8 использует PreParser для функций, которые не вызываются немедленно и не содержат потенциально опасных конструкций (например, eval
, with
, super
), поскольку PreParser не имеет возможности проанализировать такой код. Например:
const functionName = 'doSomething';
const dynamicCode = `
function ${functionName}() {
console.log('Dynamic code!');
}
${functionName}();
`
eval(dynamicCode);
V8 отдаст этот код в полноценный Parser, который сможет построить AST для анализа. А почему PreParser не справится?
Он не видит финальный код — тот создаётся динамически в строке.
Не может предсказать содержимое
eval
— там может быть что угодно.Нарушается область видимости —
eval
может добавить переменные, которых не видно на момент препарсинга.
Результатом работы PreParser является мета-информация, которую V8 использует для того, чтобы понять, нужно ли в данный момент парсить функцию полностью.
По сути, PreParser представляет собой облегчённую версию полного парсера, выполняющую минимально необходимый синтаксический анализ, достаточный для того, чтобы:
убедиться, что структура функции корректна;
собрать метаинформацию о функции;
оценить необходимость полного парсинга.
Parser
Если условия для вызова PreParser не соблюдены, или PreParser не может проанализировать функцию, V8 использует основной Parser. В отличие от упрощённого PreParser, он выполняет полный синтаксический разбор, способный обработать любые конструкции языка.
Parser понимает такие сложные элементы, как eval
, super
, with
, работает с async/await
и т.п. Он учитывает правила автоматической вставки точек с запятой (ASI), а также точно анализирует приоритет операций. Например, выражение:
a + b * c
превратится в следующую структуру:
(+)
/ \
(a) (*)
/ \
(b) (c)
Здесь *
имеет более высокий приоритет, чем +
, поэтому сначала вычисляется b * c
, а уже потом результат прибавляется к a
. Это отражается в структуре дерева: операция умножения вложена внутрь сложения.
В отличие от PreParser, Parser способен выявить все синтаксические ошибки и подготовить данные для дальнейших этапов исполнения.
Одна из ключевых особенностей Parser — генерация Abstract Syntax Tree, которое затем используется для создания байт-кода внутри Ignition. На основе полученных токенов из Scanner генерируется дерево, в котором каждый узел отражает языковую конструкцию: идентификатор, оператор, выражение или функцию.
Например, для кода...
const sum = (a, b) => a + b;
...мы получим следующее дерево:
VariableDeclaration (const)
└── VariableDeclarator
├── Identifier: sum
└── ArrowFunctionExpression
├── Params
│ ├── Identifier: a
│ └── Identifier: b
└── BinaryExpression (+)
├── Identifier: a
└── Identifier: b
Узлы AST содержат не только информацию о типе конструкции (например, BinaryExpression), но и мета-данные: позицию в исходном коде, окружение, флаги. Это важно для дебага, генерации байт-кода и возможной деоптимизации в будущем.
Примечание. Важно понимать, что структура AST не является стандартизированной — она зависит от реализации парсера в конкретной версии V8.
Сгенерировать приближенное к реальности AST можно здесь. Или заморочиться, и поставить d8, но этим мы займемся в следующих частях.
Что дальше?
На этом этапе наш код уже преобразован из текста в структурированное дерево — AST, — на основе которого V8 может производить дальнейшие действия. Это дерево полностью отражает логику программы: её переменные, выражения, операторы, функции и блоки.
Но пока это только структура — своего рода скелет программы.
Чтобы выполнить код, V8 должен преобразовать это дерево в последовательность инструкций. Этим займётся следующий участник цепочки — BytecodeGenerator, встроенный в интерпретатор Ignition. Он обойдёт AST, сгенерирует байт-код, создаст constant pool и подготовит всё для запуска. Именно с этого момента начнётся исполнение программы.
Об этом мы подробно поговорим в следующей части. И подписывайтесь на ТГ-канал JavaScript Adept автора.
Opaspap
А в v8 до сих пор выгоднее передать большой json строкой, а потом распарить его json.parse, чем вставить его просто присваиванием ? Неск лет назад так было.
Opaspap
проверил, теперь для хрома наоборот - лучше в код ставить, а для firefox - как я говорил
причем, что интересно - parse вариант на chrome значитьльно тормознее eval варианта в ff
https://jsperf.app/tikeji/1/preview