Bun — успішна революція чи невеличке повстання
Усім привіт, мене звати Олександр Зіневич, я працюю Engineering Director (Node.js, Ruby) в компанії Avenga.
Node.js сьогодні є однією з найпопулярніших технологій для веброзробки та, по суті, основним середовищем виконання JavaScript на сервері. З розвитком технологій, новими трендами у веброзробці та внаслідок критики Node.js від самого Раяна Дала (Ryan Dahl), створюються нові, альтернативні середовища виконання JavaScript на сервері, як от Deno чи Bun.
Середовище виконання Bun завдяки низці своїх особливостей та переваг позиціонується творцями як не просто гідна, а й часом беззаперечна альтернатива Node.js. Частота, з якою Bun згадується у більшості технічних інформаційних ресурсах, наштовхує на думку, що у світі серверного JavaScript відбувається революція, проте чи справді це так? Можливо, ми є свідками лише невеличкого локального повстання? У цій статті я спробую дати відповідь на ці запитання.
Що таке середовище виконання JavaScript (runtime)
Тривалий час єдиним місцем, де можна було використовувати JavaScript, був веббраузер. У 2009 році Раян Дал презентував світові нову технологію під назвою Node.js.
Node.js складався з рушія V8 (того самого, що був у браузері Google Chrome), а також набору бібліотек й інструментів для забезпечення асинхронної I/O-архітектури та взаємодії із операційною системою (сервером). Сукупність цих усіх складових і є середовищем виконання.
Минув час, і стали очевидними не тільки сильні, а й слабкі сторони платформи, які у своїй відомій доповіді 10 Things I Regret about Node.js освітив знов-таки Раян Дал.
До чого тут Bun
Поки автор Node.js активно просуває свою нову платформу Deno, Джаред Самнер (Jarred Sumner), виділивши для себе низку проблем у Node.js, вирішив створити нове середовище, яке зможе стати революційним у JavaScript-розробці на сервері.
Bun як альтернативне середовище виконання JS на сервері існує вже більше як рік. Майже весь цей час спільнота розробників та ентузіастів тестували це середовище, давала свої відгуки, долучалася безпосередньо до розробки. У вересні 2023 світ побачив офіційний реліз Bun 1.0, який, зі слів авторів, є production-ready. Bun неодноразово позиціонувався як drop-in-заміну для Node.js (інакше кажучи, що перейти з Node.js на Bun можна лише в кілька кліків та команд). Окрім цього, Bun має низку інших переваг над:
- вбудована підтримка TypeScript;
- вбудований bundler;
- вбудований раннер для тестів;
- підтримка нативного Node.js API;
- покращена швидкодія.
Як це працює
Однією з основних причин створення Bun було намагання авторів спростити розробку на Node.js і покращити швидкодію там, де це можливо. Саме тому Bun написаний з використанням низькорівневої мови програмування Zig, що забезпечило хорошу якість усіляких оптимізацій. Орім цього, Bun використовує не V8, а JavaScriptCore Engine (що застосовується у браузері Safari і також однією з основних переваг у порівнянні з V8 має швидкодію).
Через те, що з коробки Node.js не надавав ні транспайлера, ні бандлера, ні підтримки Typescript, ні багато чого іншого, це вилилося в потребу інсталювання та підтримки великої кількості сторонніх бібліотек й інструментів лише для того, щоб запустити чи зібрати проєкт.
Bun, власне, вирішує цю проблему, бо надає багато чого відразу з коробки.
На зображені ви можете бачити порівняльну схему Node.js та Bun:
Так, наприклад, Bun надає змогу відразу після інсталювання запускати файли з розширеннями: .js, .jsx, .ts, .tsx завдяки вбудованому транспайлеру. Немає потреби встановлювати додаткові бібліотеки чи інструменти — це просто працює.
Те саме стосується і підтримки CommonJS та ES-модулів. Файли з розширенням .cjs, .mjs, звісно, підтримуються відразу після встановлення Bun, але якщо ви їх не використовуєте у вашому проєкті, то require та import працюватимуть без потреби в додаткових налаштуваннях.
Це лише невеличка частинка всього того, що автоматизує та надає з коробки Bun, детальніше рекомендую почитати про це на основному вебсайті.
Чи справді все так добре
Розробники позиціонують Bun як заміну Node.js, що не вимагає додаткових конфігурацій і налаштування. Встановив — і готово!
Я вирішив перевірити, чи справді все так добре на невеличкому готовому проєкті, що використовує Node.js, Express та Mongo. Зробивши форк цього репозиторія, я відразу встановив Bun так, як про це написано в офіційній документації:
curl -fsSL <<a href="https://bun.sh/install">https://bun.sh/install</a>> | bash
Наступним кроком у відповідному фолдері з проєктом я видалив node_modules
і виконав команду bun install
.
Bun встановив усі необхідні залежності, а також згенерував свій бінарний файл bun.lockb
, який чимось схожим на yarn.lock
чи package.lock
. Тут відразу хотів би відзначити швидкість, з якою Bun встановлює всі залежності. Це відбувається справді набагато швидше, ніж під час роботи з npm
. Ця швидкість повʼязана з численними оптимізаціями, а також підходами до роботи із залежностями та кешом. Тому в інших розробників виникають труднощі та неочікувані баги, наприклад ось цей або ж цей.
Безпосередньо перед запуском я виправив команду, що відповідає за запуск у девелопмент-стані:
"dev": "cross-env NODE_ENV=development bun src/index.js",
Тепер запустити застосунок можна командою bun start
.
На жаль, очікуваного старту так і не відбулось. У консолі помилка:
error: Cannot find package "mongodb-extjson" from "/Users/…./node_modules/mongodb/lib/core/utils.js”
З такою помилкою зіштовхнувся не тільки я, а й інші розробники тут. Bun не працює не тільки з моїм поточним проєктом, а й з проєктами, написаними з використанням Nest.js фреймворку.
Додатково є проблеми і з pm2 + Bun.
У багатьох випадках причинами цих та інших проблем є, власне, не повна сумісність із Node.js Native API. В офіційній документації ви можете переглянути відповідні статуси. Автори активно працюють над тим, щоб покращувати цю сумісність, і виправляти проблеми як із Node.js Native API, так і в роботі з окремими пакетами.
Що ж зі швидкістю
Розглянемо швидкодію Bun та Node.js на кількох прикладах.
Запис, читання файлів
Використаймо невеличкий код для запису та вичитування файлів:
const fs = require('fs').promises const FILE_SIZES = [1024, 10 * 1024, 100 * 1024]; // 1KB, 10KB, 100KB const FILE_NAME_TEMPLATE = 'testfile_{size}.txt'; async function writeFile(size) { const data = Buffer.alloc(size, 'a'); // creates a buffer filled with letter 'a' const fileName = FILE_NAME_TEMPLATE.replace('{size}', size); console.time(`Writing ${size} bytes`); try { await fs.writeFile(fileName, data); console.timeEnd(`Writing ${size} bytes`); console.log(`Written ${size} bytes to ${fileName}`); } catch (err) { throw new Error(`Error writing file: ${err.message}`); } } async function readFile(size) { const fileName = FILE_NAME_TEMPLATE.replace('{size}', size); console.time(`Reading ${size} bytes`); try { const data = await fs.readFile(fileName); console.timeEnd(`Reading ${size} bytes`); console.log(`Read ${data.length} bytes from ${fileName}`); } catch (err) { throw new Error(`Error reading file: ${err.message}`); } } async function performIOOperations() { for (const size of FILE_SIZES) { await writeFile(size); await readFile(size); } } performIOOperations() .then(() => console.log('I/O operations completed')) .catch((error) => console.error(`An error occurred: ${error.message}`));
Запустивши цей код у Node.js, матимемо результати:
Цей самий код, запущений на Bun, матиме такі результати:
Слід зауважити ще одну річ, яку Bun дає нам з коробки, — це форматування виводу в термінал 😍.
З результатів можна зробити висновки, що Bun не завжди має кращу швидкодію, якщо порівнювати з Node.js. У цьому конкретному прикладі ми не вносили жодних змін у написаний код, але Bun дає нам власні API для роботи з файлами, такі як: Bun.file()
, Bun.write()
тощо. Детальніше про саме API можете почитати тут. Використавши нативне API від Bun, отримаємо такий код:
//const fs = require('fs').promises; const FILE_SIZES = [1024, 10 * 1024, 100 * 1024]; // 1KB, 10KB, 100KB const FILE_NAME_TEMPLATE = 'testfile_{size}.txt'; async function writeFile(size) { const data = Buffer.alloc(size, 'a'); // creates a buffer filled with letter 'a' const fileName = FILE_NAME_TEMPLATE.replace('{size}', size); console.time(`Writing ${size} bytes`); try { await Bun.write(fileName, data); console.timeEnd(`Writing ${size} bytes`); console.log(`Written ${size} bytes to ${fileName}`); } catch (err) { throw new Error(`Error writing file: ${err.message}`); } } async function readFile(size) { const fileName = FILE_NAME_TEMPLATE.replace('{size}', size); console.time(`Reading ${size} bytes`); try { const data = await Bun.file(fileName); console.timeEnd(`Reading ${size} bytes`); console.log(`Read ${data.length} bytes from ${fileName}`); } catch (err) { throw new Error(`Error reading file: ${err.message}`); } } async function performIOOperations() { for (const size of FILE_SIZES) { await writeFile(size); await readFile(size); } } performIOOperations() .then(() => console.log('I/O operations completed')) .catch((error) => console.error(`An error occurred: ${error.message}`));
І результати:
Використання нативного Bun API дає значно кращу швидкодію.
Сервер
Спробуємо виміряти, як буде працювати простий тестовий сервер із мок-даними, з використанням Node.js та Bun. Для прикладу використаймо такий код:
const express = require('express'); const app = express(); const PORT = 3000; // Sample in-memory data store let posts = [ { id: 1, title: 'First Post', content: 'This is the content of the first post.' }, { id: 2, title: 'Second Post', content: 'This is the content of the second post.' }, // Add more sample posts as needed... ]; // Middleware to parse JSON requests app.use(express.json()); // Fetch all posts app.get('/posts', (req, res) => { res.json(posts); }); // Fetch a single post by ID app.get('/posts/:id', (req, res) => { const post = posts.find(p => p.id === parseInt(req.params.id)); if (!post) return res.status(404).send('Post not found.'); res.json(post); }); // Create a new post app.post('/posts', (req, res) => { const post = { id: posts.length + 1, title: req.body.title, content: req.body.content, }; posts.push(post); res.status(201).json(post); }); // Update a post by ID app.put('/posts/:id', (req, res) => { const post = posts.find(p => p.id === parseInt(req.params.id)); if (!post) return res.status(404).send('Post not found.'); post.title = req.body.title || post.title; post.content = req.body.content || post.content; res.json(post); }); // Delete a post by ID app.delete('/posts/:id', (req, res) => { posts = posts.filter(p => p.id !== parseInt(req.params.id)); res.status(204).send(); }); app.listen(PORT, () => { console.log(`Server running at <a href="http://localhost">http://localhost</a>:${PORT}/`); });
Маємо такий результат для Node.js:
І такий для Bun:
Хоч такий сервер не містить комплексної логіки та працює локально, ми також можемо простежувати показники, кращі, ніж у Node.js.
Робота з даними
Розглянемо випадок, коли нам потрібно опрацювати великий об’єкт. Для цього я створив JSON-файл розміром 45 МБ, що містить масив об’єктів такого типу:
{ "userId": "b1ba31ac-25ce-4432-8e9f-b4cd89da167a", "session": "a0dd197b-22bb-4934-8c60-2408912a2a16", "timestamp": "2023-09-08T07:43:00.421Z", "activity": "LOGOUT", "ip": "214.76.47.2", "userAgent": "Mozilla/5.0 (X11; Linux i686; rv:7.9) Gecko/20100101 Firefox/7.9.5", "location": "Mohammadchester, Virginia, Andorra" }
Маємо такий код для Node.js:
import fs from 'fs/promises'; const logs = JSON.parse(await fs.readFile('../userLogs.json', 'utf-8')); function detectWindowsUsers(logs) { return logs.filter(log => log.userAgent.includes('Windows')); } console.time('detectWindowsUsers'); const windowsUsers = detectWindowsUsers(logs); console.timeEnd('detectWindowsUsers');
Запустивши його, отримаємо:
Для Bun використаємо такий код:
const logs = JSON.parse(await Bun.file('../userLogs.json', 'utf-8').text()); function detectWindowsUsers(logs) { return logs.filter(log => log.userAgent.includes('Windows')); } console.time('detectWindowsUsers'); const windowsUsers = detectWindowsUsers(logs); console.timeEnd('detectWindowsUsers');
Результат:
У цьому тесті бачимо, що результати майже однакові.
Регулярні вирази
У тому самому файлі виконаймо такий код:
import fs from 'fs/promises'; const logs = JSON.parse(await fs.readFile('../userLogs.json', 'utf-8')); function detectIPv4(logs) { const ipv4Pattern = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g; const allIPs = logs.map(log => log.ip.match(ipv4Pattern)).flat(); return [...new Set(allIPs)]; // Unique IPs } function detectChromeUsers(logs) { return logs.filter(log => /Chrome/.test(log.userAgent)); } // Benchmark IPv4 Detection console.time('IPv4 Detection'); const ipv4Addresses = detectIPv4(logs); console.timeEnd('IPv4 Detection'); console.log(`Detected ${ipv4Addresses.length} unique IPv4 addresses.`); // Benchmark Chrome User Detection console.time('Chrome User Detection'); const chromeUsers = detectChromeUsers(logs); console.timeEnd('Chrome User Detection'); console.log(`Detected ${chromeUsers.length} Chrome users.`);
Для Node.js результат буде таким:
Для Bun (з відповідними змінами у вичитуванні файлу):
У цьому випадку Bun показує значно кращу швидкодію.
Що ж у підсумку
Bun — це ще дуже молода й амбітна технологія. Її автори вибрали лозунги, з якими змогли б швидко завоювати увагу публіки. «Можливість швидкої заміни Node.js на Bun і зворотна сумісність з переважною частиною Node.js екосистеми», «швидкодія, вища в десятки разів», — хіба ж це все не прекрасно?
Прекрасно поки це лише на відео, а не на проєкті. У реальності бачимо, що легка заміна Node.js на Bun інколи вимагає великих зусиль; швидкодія багато в чому краща за Node.js, але не завжди; npm-пакети працюють, але не всі; а на момент написання цієї статті в репозиторії Bun понад 1800 відкритих тікетів.
Це все типові проблеми для молодої технології, та творці Bun зробили неймовірну роботу. Попереду ще більше праці, оскільки, крім простого фіксу багів, необхідно сформувати повноцінне комʼюніті, яке саме буде підтримувати та розвивати Bun. Попри таку популярність, зараз список основних контрибуторів не надто великий, а основним з них є власне творець Bun.
Також важливо, щоб хтось з великих компаній, які ризикнуть використати цю технологію, допомогли сформувати відповідні маркетингові кейси для подальшої популяризації.
Тому на сьогодні Bun — це лише невеличке локальне повстання, яке здатне перерости в революцію. Уже зараз Bun можна використовувати в тестовому режимі для окремих утиліт, окремих маленьких підпроєктів, але зарано повністю мігруватись і використовувати його для великих кодових баз.
36 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів