Що таке runtime і чому код не працює скрізь однаково

Є одна річ, яка часто плутає новачків: ти пишеш код на JavaScript, він ідеально працює в браузері — а коли намагаєшся запустити його на сервері, нічого не відбувається. Або навпаки: скрипт працює в Node.js, але в браузері падає з помилкою про відсутню функцію.

Проблема не в коді. Проблема в тому, де цей код запускається. І ця «де» — це і є runtime (середовище виконання).

Проста analogія: JavaScript — це мова. А runtime — це країна, де ти нею говориш. Граматика та сама, але закони, культура і доступні сервіси — різні.

Проблема / контекст: чому «воно ж працює в мене» — це не те саме

JavaScript спочатку створювався для браузерів. Це мова, яка жила всередині вебсторінки і могла маніпулювати DOM, реагувати на кліки, міняти кольори. Але вона не вміла читати файли, не вміла працювати з мережою напряму, не мала доступу до файлової системи.

Потім з’явився Node.js — і JavaScript вийшов за межі браузера. На сервері він отримав доступ до файлів, мережі, баз даних — але втратив DOM. Тут немає window чи document. Це вже інший світ.

Потім з’явилися й інші: Bun, Deno, Cloudflare Workers — і кожен має свої особливості.

Той самий синтаксис JavaScript працює в них усіх. Але набір інструментів, які йому доступні — різний. І саме тут виникають помилки.

Як це працює простими словами

Runtime — це три речі:

1. JavaScript-двигун

Це те, що власне розуміє і виконує JavaScript-код. Більшість сучасних runtime використовують один із трьох основних двигунів:

Двигун парсить (читає) твій код, трансформує його в машинні інструкції, оптимізує і запускає.

2. Глобальні об’єкти і API

Ось тут починаються відмінності між runtime:

У браузері:

window          // глобальний об'єкт браузера
document        // DOM — вся сторінка
fetch()         // мережеві запити
setTimeout()    // таймери
localStorage    // зберігання даних на стороні клієнта

У Node.js:

global          // глобальний об'єкт (замість window)
require()       /import файлів
fs              // доступ до файлової системи
http            /создание сервера
process         // інформація про процес (PID, змінні оточення)

У Bun:

Bun.serve()     // вбудований вебсервер
Bun.file()      // зручна робота з файлами
fetch()         // працює вбудовано, без залежностей

Безпосередньо JavaScript (змінні, функції, класи) працює скрізь однаково. А от API — ні.

3. Модульна система і залежності

Кожен runtime по-різному підходить до питання «як підключити код з іншого файлу».

Навіщо це потрібно на практиці

1. Вибір інструменту під задачу

Якщо ти робиш сервер — тобі підійде Node.js або Bun. Якщо працюєш з frontend-кодом — браузер сам по собі runtime. Якщо запускаєш функції в хмарі — можливо Cloudflare Workers або AWS Lambda.

Не існує «правильного» runtime. Існує «відповідний для конкретної задачі».

2. Розуміння, чому код не працює

Коли ти розумієш різницю між runtime-ами, стає очевидною природа багатьох помилок:

3. Продуктивність

Різні runtime мають різну продуктивність. Node.js — зрілий, з найбільшою екосистемою. Bun — значно швидший запуску, менше пам’яті, але молодший. Deno — має вбудовану безпеку і TypeScript з коробки.

Для маленького скрипту продуктивність не грає ролі. Для високонавантаженого сервера — може бути критичною.

Практичний приклад: один і той самий код, різні runtime

Ось простий файл:

// hello.js
console.log("Привіт!");
console.log(typeof window);    // чи є browser API?
console.log(typeof process);   // чи є Node.js API?
console.log(typeof Bun);       // чи є Bun API?

Запускаємо в браузері (через devtools console):

Привіт!
object      // window — об'єкт браузера
undefined   // process — немає
undefined   // Bun — немає

Запускаємо в Node.js:

Привіт!
undefined   // window — немає
object      // process — є
undefined   // Bun — немає

Запускаємо в Bun:

Привіт!
undefined   // window — немає
object      // process — є (Bun емілює Node.js API)
object      // Bun — є

Той самий код, три різних результати. Не через помилку — через різне оточення.

Як обрати runtime для свого проєкту

Питання не в тому, який runtime “найкращий”. Питання в тому, що тобі потрібно:

Node.js — якщо:

Це стандартний вибір. Він не завжди найшвидший, але завжди найбезпечніший.

Bun — якщо:

Bun набирає популярність, але він молодший. Не все в npm працює з ним без проблем.

Deno — якщо:

Deno цікавий концептуально, але його екосистема менша.

Типові помилки

1. Припускати, що npm-пакет працює скрізь

Не кожен npm-пакет універсальний. Якщо пакет використовує fs або process, він не працюватиме в браузері чи Cloudflare Workers. Завжди перевіряй документацію пакета.

2. Плутати мову зі середовищем

JavaScript ≠ Node.js. Це дві різні речі. JavaScript — це мова (синтаксис, оператори, функції). Node.js — це одне з середовищ, де ця мова виконується, і воно додає свої API.

3. Ігнорувати версії

Node.js 14, 18, 20 — це різні runtime з різними можливостями. Те, що працює в Node.js 20, може не працювати в 14. Завжди перевіряй мінімальну підтримувану версію.

4. Використовувати неправильний runtime для задачі

Браузер не підходить для серверної логіки. Node.js не підходить для frontend-рендерингу (без додаткових інструментів). Cloudflare Workers не підходить для задач, що потребують тривалого виконання.

Висновок / план дії

Runtime визначає, які інструменти доступні твоєму коду. Розуміння цієї різниці — ключ до того, щоб «воно працює» не було сюрпризом або проблемою.

Що зробити найближчим часом:

  1. З’яси, який runtime зараз використовуєш (браузер, Node.js, чи щось інше).
  2. Запусти простий JS-файл у двох різних runtime-ах і порівняй вивід.
  3. Перевір, чи використовуєш ти залежності, які не працюють у твоєму runtime.
  4. Якщо проєкт новий — розгляни альтернативи (Bun, Deno) і порівняй продуктивність.
  5. Додай перевірку версії runtime в скрипти збирання, щоб уникнути несподіванок.

Коли ти перестанеш думати «JavaScript скрізь однаковий» і почнеш думати «який runtime мені потрібен» — рівень розуміння вирастет відчутно.