Размер шрифта:
Ленивый синтаксический анализ JavaScript в V8
Play

Ленивый синтаксический анализ JavaScript в V8

В движках JavaScript для ускорения выполнения кода используется механизм, называемый ленивым синтаксическим анализом (lazy parsing).

В этой статье вы узнаете: что такое ленивый синтаксический анализ, чем он полезен и каковы потенциальные проблемы при его использовании.

Исходные данные

V8 — это JavaScript-движок, который используется в Chrome и Node. Хотя эта статья посвящена V8, это не единственный JavaScript-движок, использующий ленивый синтаксический анализ.

Перед запуском JavaScript его необходимо преобразовать в машинный код. Это то, что делает V8.

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

Синтаксический анализ — это второй шаг, преобразование токенов в абстрактное синтаксическое дерево (AST). Вот пример исходного кода с соответствующим ему AST:

Если вы хотите попробовать поиграть с синтаксическими деревьями JavaScript, попробуйте AST Explorer.

Предварительный и полный синтаксический анализ

Для синтаксического анализа требуется время, поэтому JavaScript-движки стараются избегать глубокого разбора файла.

Это возможно, потому что большинство функций в файле JavaScript никогда не вызываются или, например, не будут вызваны до тех пор, пока пользователь не начнёт взаимодействовать с пользовательским интерфейсом.

Поэтому вместо анализа каждой функции, большинство функций подвергаются предварительному анализу. Предварительный анализ обнаруживает синтаксические ошибки, но не разрешает область видимости переменных, используемых в функции, и не генерирует для неё AST.

Благодаря менее интенсивному анализу, предварительный синтаксический анализ примерно в два раза быстрее, чем полный.

Однако, когда вы вызываете функцию, которая еще не была полностью проанализирована, вам нужно выполнить полный анализ во время её вызова.

Пример с использованием V8

Давайте посмотрим на пример такого поведения.

Node имеет специальный аргумент --trace_parse , при запуске с которым вам будет видно, как происходит синтаксический анализ скриптов или функции. Тем не менее, вывод может быть довольно большим из-за различного внутреннего кода, который Node запускает для начальной загрузки вашей программы. Поэтому вместо Node я буду использовать оболочку V8 под названием d8.

В отличие от Node, d8 не имеет функции console.log , поэтому я использую функцию с именем print :

Здесь у меня две функции sayHi и add , причём add никогда не вызывается.

Всё еще присутствует избыточный вывод, связанный с d8 , но гораздо меньший, чем если бы мы использовали Node (при использовании d8 из последней сборки v8 избыточный вывод отсутствует, - прим. пер.). Важны три последние строчки.

Когда test.js первоначально анализируется, функции sayHi и add обрабатываются только предварительно, что ускоряет анализ исходного кода.

Тогда же, когда мы вызываем sayHi , функция анализируется полностью.

Важно: add никогда не анализируется полностью. Это позволяет экономить время синтаксического анализатора и уменьшает потребление памяти V8.

Если мы добавим add(1, 2) к нашему JavaScript-коду, функция add будет проанализирована во время вызова:

В чём проблема с ленивым синтаксическим разбором?

Давайте уберём неиспользуемую функцию add из кода ниже.

Вывод d8 --trace_parse test.js :

Сначала V8 предварительно обрабатывает sayHi , после чего следует полный синтаксический анализ. Тогда предварительный анализ не нужен и наша программа работает быстрее без попыток оптимизации V8!

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

В V8 фактически есть эвристика, где функции, заключенные в круглые скобки, всегда анализируются полностью. Например, это относится к немедленно вызываемым функциональным выражениям (IIFE):

Обратите внимание: здесь нет parsing function: constants .

Микрооптимизации JavaScript-кода

Теперь предположим, что мы хотим, чтобы наш пример sayHi работал быстрее. Что мы можем сделать?

Во-первых, давайте сохраним sayHi как переменную вместо использования объявления функции:

V8 по-прежнему выполняет предварительный парсинг sayHi , но мы можем предотвратить это, завернув выражение функции в круглые скобки.

Хотя это и не IIFE, V8 будет использовать эвристику и сделает полный анализ с самого начала:

Optimize-JS

Вместо того, чтобы вручную делать эти оптимизации и превращать наш код в трудно читаемый, мы можем использовать инструмент optimize-js.

На практике общей причиной ненужных предварительных анализов является то, что минификатор UglifyJS удаляет круглые скобки из IIFE для сохранения байтов:

Это не меняет поведение кода, но нарушает эвристику Chrome.

Если вы запустите optimize-js на уменьшенном коде выше, скобки будут восстановлены:

Кстати, readme optimise-js уточняет, что Chakra, движок JavaScript, используемый в Edge, способен правильно определять синтаксис !Function ()() IIFE и предотвращать предварительный анализ.

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

Стоят ли эти оптимизации потраченных усилий?

В документации optimize-js есть результаты несколько тестов производительности, показывающие впечатляющие ускорения - около 20%. Однако в Chrome на моем ноутбуке фактическое улучшение для образца приложения на React составляет всего 6 мс (время загрузки кода уменьшилось с 24мс до 18мс).

Ускорение будет заметнее на более медленных устройствах. Но не на iPhone: движок Safari, JavaScriptCore, не показывает улучшения для оптимизированного кода.

Скорее всего, есть более эффективные вещи, которые вы можете сделать для повышения производительности своих сайтов. Но если у вас закончились идеи, стоит попробовать optimize-js и измерить, есть ли какое-то значимое улучшение.

📎📎📎📎📎📎📎📎📎📎