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


В этой статья я немного расскажу вам о 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!




Комментарии (5)


  1. domix32
    12.01.2023 12:00
    +1

    я что-то делаю не так?

    вангую, что это из-за того что в Bun ещё нет толком оптимизаторов - V8 наверняка сделал и оптимизацию хвостовой рекурсии и горячий путь поJITил спустя пару десятков циклов. Имело смысл попробовать сделать ещё один "прогретый" прогон не отходя кассы, чтобы убедиться в этом.

    Интересно какой у bun memory print в сравнении с нодой.


    1. ermouth
      12.01.2023 22:28

      Увы, V8 действительно когда-то сделал оптимизацию хвостовой рекурсии – а потом откатил. Вот тут история https://bugs.chromium.org/p/v8/issues/detail?id=4698

      Я причём с 2016 по прошлый год был уверен, что она там есть – и очень обжёгся на этом.


      1. rock
        13.01.2023 00:56

        В отличии от V8 -> Node, в JavaScriptCore -> Bun TCO (вернее, PTC) как раз есть, см. верхняя строчка, столбцы Safari, здесь. А в V8 она была разве что экспериментальной, под флагом.


  1. AlexGorky
    12.01.2023 13:37

    Странно, но для Эппл есть инсталлятор, а для Windows нет.


    1. domix32
      12.01.2023 15:01
      +1

      Потому что оно работает только в WSL, а там уже обычные линукса и этот их blabla.sh|bash