Як працює компіляція JavaScript

Автор: Masroor Hosseini

JavaScript — одна з найпопулярніших мов програмування, передусім завдяки своїй ролі у веб-розробці. Спочатку це була інтерпретована мова, що означає, що браузер читав та виконував код JavaScript лінія за лінією. Однак з розвитком сучасних движків JavaScript процес змістився до компіляції та оптимізації. У цій статті ми розглянемо, як працюють компілятори JavaScript, зокрема, зосередимося на концепціях компіляційного процесу.

Інтерпретовані vs. Компільовані мови

Перш ніж зануритись у деталі компіляції JavaScript, важливо зрозуміти різницю між інтерпретованими та компільованими мовами:

Інтерпретовані мови: Код виконується лінія за лінією інтерпретатором, без попереднього перетворення в машинний код. Це дозволяє реалізувати динамічну поведінку, але часто призводить до повільнішого виконання.
Компіляційні мови: Код переводиться в машинний код до його виконання. Це зазвичай забезпечує швидше виконання, оскільки процесор може безпосередньо розуміти машинний код.

JavaScript знаходиться на середньому шляху. Історично він був інтерпретований браузерами, але сучасні движки, такі як V8 від Google (який використовується в Chrome та Node.js), ввели компіляцію Just-In-Time (JIT) для підвищення продуктивності.

Движок JavaScript: основа компіляції

Компільтори JavaScript є частиною того, що називається движком JavaScript. Кожен браузер має свій власний движок JavaScript:

  • V8: Google Chrome та Node.js
  • SpiderMonkey: Mozilla Firefox
  • Chakra: Microsoft Edge (до переходу на Chromium)
  • JavaScriptCore: Safari

Всі ці движки реалізують стандарт ECMAScript, який визначає, як має працювати JavaScript. Давайте розглянемо етапи, через які проходить типовий движок JavaScript, щоб виконати код.

Як працює компіляція JavaScript

Парсинг вихідного коду
Перший етап компіляції — це парсинг. Движок розбиває код JavaScript на абстрактне синтаксичне дерево (AST) через два етапи:

  1. Лексичний аналіз (Токенізація): Код JavaScript розбивається на маленькі частини, які називаються токенами. Кожен токен представляє базові елементи, такі як ключові слова, імена змінних, оператори тощо.
  2. Синтаксичний аналіз: Токени організовуються в деревоподібну структуру, що називається абстрактним синтаксичним деревом (AST). Це дерево відображає ієрархічну структуру програми.

let x = 10;

Цей код буде розбитий на токени, такі як let, x, =, і 10, а потім організований у AST, щоб зрозуміти, як змінна x отримує значення 10.

  1. Проміжне подання (IR)
    Після побудови AST, движок перетворює його в проміжне подання (IR). Це абстрактний рівень коду машини, який легший для оптимізації. IR слугує містком між вихідним кодом і машинним кодом, допомагаючи застосовувати різні оптимізації до остаточного виконання.

  2. Компіляція Just-In-Time (JIT)
    Сучасні движки JavaScript використовують техніку, яку називають компіляцією Just-In-Time (JIT), для оптимізації продуктивності. JIT компілятори беруть частини коду і компілюють їх у машинний код прямо перед їхнім використанням. Це забезпечує переваги як інтерпретованих, так і компільованих мов.

  • Базовий компілятор: Базовий JIT компілятор спочатку швидко компілює код JavaScript у машинний код без глибокої оптимізації. Це дозволяє швидко виконувати код, але може бути не найефективнішим.
  • Оптимізація та деоптимізація: Движок потім моніторить продуктивність коду під час виконання. Якщо він помічає часто виконуваний код (так званий “гарячий” код), він додатково оптимізує цей фрагмент, застосовуючи передові техніки, такі як вбудовування функцій або зменшення зайвих операцій.
    Деоптимізація: Якщо припущення, зроблені під час оптимізації, виявляються неправильними (наприклад, змінна була припущена як завжди число, але згодом стає рядком), движок може деоптимізувати код і повернути його до менш оптимізованої версії.
  1. Збір сміття
    Движки JavaScript автоматично керують пам'яттю за допомогою процесу, відомого як збір сміття. Цей процес виявляє об'єкти, які більше не використовуються, і звільняє пам'ять. Сучасні движки використовують стратегії, такі як Mark-and-Sweep і Generational Garbage Collection, щоб ефективно керувати пам'яттю, забезпечуючи безперервну роботу програми без витоків пам'яті.

Приклад: Движок V8
Давайте розглянемо, як движок V8 від Google реалізує цей процес.

  • Ignition: V8 використовує компонент під назвою Ignition для генерації байт-коду з JavaScript. Байт-код є нижчим рівнем подання вихідного коду, який все ще абстрактний, але його легше виконувати, ніж сирий JavaScript.
  • Turbofan: Якщо якась частина байт-коду виконується часто, движок V8 використовує свій оптимізуючий компілятор Turbofan для подальшої компіляції цього байт-коду в високоефективний машинний код.
  • Inline Caching: Інша техніка, яку використовує V8, — це inline caching (вбудоване кешування), яке запам'ятовує типи об'єктів і операцій у часто виконуваних функціях. Це допомагає в оптимізації коду, роблячи менше припущень про поведінку коду, що веде до швидшого виконання.

Ключові оптимізації в компіляції JavaScript

  • Inlining: Заміна викликів функцій на тіло функції, щоб зменшити накладні витрати.
  • Спеціалізація типів: Припущення щодо типів змінних для генерації більш ефективного коду.
  • Видалення непотрібного коду: Видалення коду, який ніколи не виконується.
  • Лінива компіляція: Компіляція лише тих частин коду, які насправді використовуються.

Висновок

Перехід JavaScript від чисто інтерпретованої мови до такої, що значною мірою покладається на компіляцію Just-In-Time (JIT), значно покращив його продуктивність. Сучасні движки JavaScript, такі як V8, поєднують кілька технік для парсингу, оптимізації та виконання коду, що дозволяє JavaScript виконувати складні додатки в браузерах і серверних середовищах. Розуміння того, як працюють ці движки, дає розробникам можливість писати більш ефективний, оптимізований код, який максимально використовує можливості движка.

Перекладено з: How JavaScript Compilation Works

Leave a Reply

Your email address will not be published. Required fields are marked *