Skip to main content

Краткий обзор Bun — новой среды выполнения JavaScript

· 9 min read

Привет, друзья!

В этой статья я немного расскажу вам о Bun - новой среде выполнения JavaScript-кода.

Обратите внимание: Bun - это экспериментальная штуковина, поэтому использовать ее для разработки производственных приложений пока не рекомендуется.

К слову, в рейтинге "Восходящие звезды JavaScript 2022" 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!