Привет, друзья!
В этой статья я немного расскажу вам о Bun — новой среде выполнения JavaScript-кода.
Обратите внимание: Bun — это экспериментальная штуковина, поэтому использовать ее для разработки производственных приложений пока не рекомендуется.
Интересно? Тогда прошу под кат.
Что такое Bun?
Bun — это современная среда выполнения JS типа Node.js или Deno со встроенной поддержкой JSX и TypeScript. Она разрабатывалась с акцентом на трех вещах:
- быстрый запуск;
- высокая производительность;
- самодостаточность.
Bun включает в себя следующее:
- реализацию веб-интерфейсов вроде fetch, WebSocket и ReadableStream;
- реализацию алгоритма разрешения
node_modules
, что позволяет использовать пакеты npm в Bun-проектах. Bun поддерживает как ES, так и CommonJS-модули (сам Bun использует ESM); - встроенную поддержку JSX и TS;
- встроенную поддержку
"paths"
,"jsxImportSource"
и других полей из файлаtsconfig.json
; - API
Bun.Transpiler
— транспилятора JSX и TS; -
Bun.write
для записи, копирования и отправки файлов с помощью самых быстрых из доступных возможностей файловой системы; - автоматическую загрузку переменных среды окружения из файла
.env
; - встроенного клиента SQLite3 (
bun:sqlite
); - реализацию большинства интерфейсов Node.js, таких как fs, path и Buffer;
- интерфейс внешней функции с низкими накладными расходами
bun:ffi
для вызова нативного кода из JS.
Bun использует движок JavaScriptCore, разрабатываемый WebKit, который запускается и выполняет операции немного быстрее, а также использует память немного эффективнее, чем классические движки типа V8. Bun написан на Zig — языке программирования низкого уровня с ручным управлением памятью, чем объясняются высокие показатели его скорости.
Большая часть составляющих Bun была реализована с нуля.
Таким образом, Bun это:
- среда выполнения клиентского и серверного JS;
- транспилятор JS/JSX/TS;
- сборщик JS/CSS;
- таскраннер (task runner) для скриптов, определенных в файле
package.json
; - совместимый с npm менеджер пакетов.
Впечатляет, не правда ли?
Примеры использования Bun
Рассмотрим несколько примеров использования Bun для разработки серверных и клиентских приложений.
Начнем с установки.
Установка
Для установки Bun достаточно открыть терминал и выполнить следующую команду:
curl -fsSL https://bun.sh/install | bash
Обратите внимание: для установки Bun в Windows требуется WSL (Windows Subsystem for Linux — подсистема Windows для Linux). Для ее установки необходимо открыть PowerShell в режиме администратора и выполнить команду wsl --install
, после чего — перезагрузить систему и дождаться установки Ubuntu. После установки Ubuntu открываем приложение wsl и выполняем команду для установки Bun.
Проверить корректность установки (версию) Bun можно с помощью команды bun --version
(в моем случае — это 0.4.0
).
Чтение файла
Создаем директорию bun
, переходим в нее и создаем файлы hello.txt
и cat.js
:
mkdir bun
cd ./bun
touch hello.txt cat.js
Редактируем hello.txt
:
Всем привет! ;)
Редактируем cat.js
:
// модули Node.js
import { resolve } from 'node:path'
import { access } from 'node:fs/promises'
// модули Bun
import { write, stdout, file, argv } from 'bun'
// читаем путь из ввода
// bun ./cat.js [path-to-file]
const filePath = resolve(argv.at(-1))
let fileContent = 'Файл не найден\n'
// если при доступе к файлу возникла ошибка,
// значит, файл отсутствует
try {
await access(filePath)
// file(path) возвращает `Blob`
// https://developer.mozilla.org/en-US/docs/Web/API/Blob
fileContent = file(filePath)
} catch {}
await write(
// стандартным устройством вывода является терминал,
// в котором выполняется команда
stdout,
fileContent
)
Выполняем команду bun ./cat.js ./hello.txt
:
Выполняем команду bun ./cat.js ./hell.txt
:
HTTP-сервер
Создаем файлы index.html
и http.js
:
mkdir bun
cd ./bun
touch index.html http.js
Редактируем index.html
:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Страница приветствия</title>
</head>
<body>
<h1>Всем привет! ????</h1>
</body>
</html>
Редактируем http.js
:
import { resolve } from 'node:path'
import { file } from 'bun'
// адрес страницы приветствия
const INDEX_URL = 'http://localhost:3000/'
// путь к файлу `index.html`
const filePath = resolve(process.cwd(), './index.html')
// содержимое `index.html`
const fileContent = file(filePath)
export default {
// порт
// дефолтный, можно не указывать явно
port: 3000,
// обработчик запросов
fetch(request) {
console.log(request)
// если запрашивается страница приветствия
if (request.url === INDEX_URL) {
// возвращаем `index.html`
return new Response(fileContent, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
}
})
}
// иначе возвращаем такой ответ
return new Response('Запрашиваемая страница отсутствует', {
status: 404
})
}
}
Выполняем команду bun ./http.js
для запуска сервера и переходим по адресу http://localhost:3000
:
Пробуем перейти по другому адресу, например, http://localhost:3000/test
:
Объекты выполненных нами запросов выглядят следующим образом:
Чат
Создаем файлы ws.html
и ws.js
:
touch ws.html ws.js
Редактируем ws.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Чат</title>
<style>
.msg-form {
display: none;
}
</style>
</head>
<body>
<main>
<form class="name-form">
<label>Имя: <input type="text" required autofocus /></label>
<button>Подключиться</button>
</form>
<form class="msg-form">
<label>Сообщение: <input type="text" required /></label>
<button>Отправить</button>
</form>
<ul></ul>
</main>
<script>
// ссылки на формы, инпуты и список
const main = document.querySelector('main')
const nameForm = main.querySelector('.name-form')
const nameInput = nameForm.querySelector('input')
const msgForm = main.querySelector('.msg-form')
const msgInput = msgForm.querySelector('input')
const msgList = main.querySelector('ul')
// переменная для сокета
let ws
// обработка отправки формы для имени
nameForm.addEventListener(
'submit',
(e) => {
e.preventDefault()
const name = nameInput.value
// открываем соединение
// имя передается в качестве параметра строки запроса
ws = new WebSocket(`ws://localhost:3000?name=${name}`)
ws.onopen = () => {
console.log('Соединение установлено')
// регистрируем обработчик сообщений
ws.onmessage = (e) => {
// добавляем элемент в конец списка
const msgTemplate = `<li>${e.data}</li>`
msgList.insertAdjacentHTML('beforeend', msgTemplate)
}
}
nameForm.style.display = 'none'
msgForm.style.display = 'block'
msgInput.focus()
},
{ once: true }
)
// обработка отправки формы для сообщения
msgForm.addEventListener('submit', (e) => {
// проверяем, что соединение установлено
if (ws.readyState !== 1) return
e.preventDefault()
const msg = msgInput.value
// отправляем сообщение
ws.send(msg)
msgInput.value = ''
})
</script>
</body>
</html>
Редактируем ws.js
:
export default {
fetch(req, server) {
if (
server.upgrade(req, {
// этот объект доступен через `ws.data`
data: {
name: new URL(req.url).searchParams.get('name') || 'Friend'
}
})
)
return
return new Response('Ожидается ws-соединение', { status: 400 })
},
websocket: {
// обработка подключения
open(ws) {
console.log('Соединение установлено')
// подписка на `chat`
ws.subscribe('chat')
// сообщаем о подключении нового пользователя всем подключенным пользователям
ws.publish('chat', `${ws.data.name} присоединился к чату`)
},
// обработка сообщения
message(ws, message) {
// передаем сообщение всем подключенным пользователям
ws.publish('chat', `${ws.data.name}: ${message}`)
},
// обработка отключения
close(ws, code, reason) {
// сообщаем об отключении пользователя
ws.publish('chat', `${ws.data.name} покинул чат`)
},
// сжатие
perMessageDeflate: true
}
}
Выполняем команду bun --hot ./ws.js
для запуска сервера. Флаг --hot
указывает Bun перезапускать сервер при изменении ws.js
(что-то типа nodemon для Node.js, работает не очень стабильно).
Открываем ws.html
в 2 вкладках браузера, подключаемся к серверу и переписываемся:
Сравнение производительности Node.js и Bun
Создаем файл test.js
следующего содержания:
console.time('test')
for (let i = 0; i < 10000; i++) console.log(i)
console.timeEnd('test')
Для чистоты эксперимента я установил Node.js 18 версии в wsl с помощью команды curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs
.
Выполнение кода test.js
с помощью Bun занимает 15-30 мс
:
А с помощью Node.js — 115-125 мс
:
Кажется, что вывод о более высокой производительности Bun по сравнению с Node.js очевиден, но давайте не будем спешить.
Перепишем код test.js
следующим образом:
// 10 раз вычисляем факториал числа 10
// и получаем среднее время выполнения операций
const diffArr = []
for (let i = 0; i < 10; i++) {
const now = performance.now()
const factorial = (n) => (n ? n * factorial(n - 1) : 1)
factorial(10)
const diff = performance.now() - now
diffArr.push(diff)
}
const avg = diffArr.reduce((a, b) => a + b, 0) / diffArr.length
console.log(avg)
Выполнение этого кода с помощью Bun занимает в среднем 0,025 мс
:
А с помощью Node.js — 0,002 мс
:
Получается, что при работе с выводом (stdout) Bun производительнее Node.js в 7 раз (115 / 15 = 7,66...
), а при выполнении вычислительных операций (по крайней мере, когда речь идет о рекурсии) Node.js производительнее Bun в 12 раз (0,025 / 0,002 = 12.5
) (я что-то делаю не так? Поделитесь своим мнением на этот счет в комментариях).
Создание шаблона React-приложения
Теперь поговорим об использовании Bun для разработки клиентских приложений.
В настоящее время Bun предоставляет только один готовый шаблон — для React. На подходе шаблон для Next.js, но там еще много всего не реализовано. Bun также можно использовать с SPA на чистом JS.
Для создания шаблона React-приложения с помощью Bun достаточно выполнить следующую команду:
# app-name - название приложения/директории
bun create react [app-name]
Создаем проект react-app-bun
:
bun create react react-app-bun
Выполнение этой операции занимает 10,6 сек
:
Для сравнения, создание проекта с помощью Create React App (npx create-react-app react-app-cra
) занимает больше 2 мин
:
Однако Vite демонстрирует очень близкий показатель скорости:
yarn create vite react-app-vite --template vanilla && \
cd ./react-app-vite && \
yarn
Yarn идет в комплекте с Node.js и npm.
Команда yarn create vite
создаст директорию с файлами без установки зависимостей.
Переходим в директорию react-app-bun
и запускаем сервер для разработки:
cd ./react-app-bun
bun dev
Запуск происходит практически мгновенно. Справедливости ради следует отметить, что запуск сервера для разработки с помощью Vite также происходит очень быстро, чего не скажешь о CRA.
Для добавления npm-пакетов используется команда bun install
или bun add
(для установки зависимостей для разработки используется флаг --development
или -d
), для удаления — bun remove
.
Для выполнения команд, определенных в разделе scripts
файла package.json
используется команда bun run [command-name]
(run
можно опустить).
Что касается TS, то в настоящее время типы для Bun находятся в пакете bun-types:
bun add -d bun-types
tsconfig.json
:
{
"compilerOptions": {
"types": ["bun-types"]
}
}
Пожалуй, это все, что я хотел рассказать вам о Bun.
На мой взгляд, основным преимуществом Bun является то, что он объединяет в себе целую кучу инструментов, которые используются для разработки современных веб-приложений, и при этом демонстрирует очень высокие показатели скорости. Поэтому я буду с нетерпением ждать релиза его стабильной версии.
Надеюсь, вы узнали что-то новое и не зря потратили время.
Благодарю за внимание и happy coding!
domix32
вангую, что это из-за того что в Bun ещё нет толком оптимизаторов - V8 наверняка сделал и оптимизацию хвостовой рекурсии и горячий путь поJITил спустя пару десятков циклов. Имело смысл попробовать сделать ещё один "прогретый" прогон не отходя кассы, чтобы убедиться в этом.
Интересно какой у bun memory print в сравнении с нодой.
ermouth
Увы, V8 действительно когда-то сделал оптимизацию хвостовой рекурсии – а потом откатил. Вот тут история https://bugs.chromium.org/p/v8/issues/detail?id=4698
Я причём с 2016 по прошлый год был уверен, что она там есть – и очень обжёгся на этом.
rock
В отличии от V8 -> Node, в JavaScriptCore -> Bun TCO (вернее, PTC) как раз есть, см. верхняя строчка, столбцы Safari, здесь. А в V8 она была разве что экспериментальной, под флагом.