Релиз Bun v1.0 вызвал ажиотаж и споры в JavaScript-сообществе. Все начали сравнивать, что лучше: Bun или Node.js? Разбираемся вместе с Сергеем Константиновым, наставником на курсе «Фронтенд-разработчик».

Bun — это новая среда исполнения Javascript-кода, по сути аналог Node.js. Давайте сравним их начиная с общего обзора, заканчивая производительностью по различным метрикам и в разных ситуациях.

С чего всё началось

Node.js появился в 2009 году и решал задачу исполнения JavaScript-кода на стороне сервера. С того времени и JS, и Node претерпели много изменений.

23 сентября 2023 года вышел релиз Bun 1.0. Bun, как и Node, позволяет запускать JavaScript-код на стороне сервера. Действительно ли молодой Bun настолько хорош? И насколько Node может с ним конкурировать?

Обсуждая среды исполнения JS-кода, сложно обойти стороной Deno. Он был создан Раеном Далем (создателем Node.js), чтобы исправить ряд проблем, которые обнаружили в Node.

Deno стал безопасной средой исполнения JavaScript-кода. Например, он поддерживает TypeScript из коробки — это избавляет от необходимости лишних внешних зависимостей. Deno использует подход, ориентированный на безопасность. Это требует от разработчиков предоставлять разрешения при потенциально конфиденциальных операциях: доступ к файловой системе или сетевые подключения. Хотя Deno и выглядит привлекательной альтернативой Node.js, он не получил такого широкого распространения.

Основное внимание в статье будет уделено сравнению Node.js и Bun, но мы также рассмотрим и Deno в ряде кейсов.

Bun vs Node: общий обзор

Node.js преимущественно написан на языке C++, а для Bun выбрали язык Zig. В своей архитектуре экосистема Node.js использует модульную парадигму. То есть для работы с WebSocket, TypeScript, тестирования ему необходимы библиотечные модули.

Напротив, Bun представляет собой среду «всё в одном»: поддержка WebAPI, пакетный менеджер, инструменты для тестирования и многое другое. Данные модули реализованы внутри Bun и, по словам разработчиков рантайм-среды, лучше оптимизированы, чем аналоги в Node.js.

Движок JavaScript

  • Node.js использует движок Google V8, который также лежит в основе браузера Chrome. Bun отдал своё предпочтение JavaScriptCore, который является достоянием Apple и лежит в основе WebKit (Safari).
  • V8 и JSC имеют в своей реализации различные архитектуры и используют разные подходы к оптимизации.
  • JSC отдаёт приоритет более быстрому запуску и уменьшению использования памяти при более медленном времени выполнения.
  • С другой стороны, V8 отдаёт приоритет быстрому выполнению с большим числом оптимизаций для последующей работы в рантайме, что также может привести к большему использованию памяти.

Благодаря этому Bun способен запускаться в 4 раза быстрее, чем Node.js:

Источник: https://habr.com/ru/news/760006/

На самом деле такой тест не говорит о превосходстве в скорости JSC по сравнению с V8 — каждая из сред имеет свои дополнительные обработки и проверки. Но можно провести сравнение JSC и V8 изолированно. Я воспользовался результатами Криса Хэй из его ролика на Youtube. Там он реализовал простой компилятор на Rust, для использования V8, а также JSCore, который можно вызвать на любом компьютере от Apple.

Вот временные результаты прогона теста скорости запуска на “hello world”:

Время запуска
JSCore 1.12 ± 0.18
Bun 2.68 ± 0.37
Deno 4.29 ± 0.55
Node 9.27 ± 1.17

Мы видим, что JSC действительно работает быстрее и V8, и Bun. Значит, потенциально у Bun есть возможности для больших оптимизаций.

Транспайлер TypeScript

Несмотря на свою популярность, Node.js не имеет встроенной поддержки TypeScript. Один из вариантов использования TypeScript на Node — установка модуля для TS и его настройка для транспиляции в JS с последующим исполнением.

Пример базовой настройки c использованием TS-Node:

  1. Установка
npm install --save-dev typescript ts-node

2. Конфигурация стартового скрипта. В package.json установите следующий скрипт запуска приложения:

{
  "scripts": {
    "start": "ts-node ./path/to/your/file.ts"
  }
}

3. Запуск. С помощью описанного выше сценария вы сможете запускать приложение, написанное на TypeScript.

npm start

Bun имеет интегрированный транспайлер, позволяющий напрямую запускать файлы .js, .ts, .jsx и .tsx, без дополнительной настройки. Также это позволяет бесшовно производить транспиляцию JavaScript-файлов — это обеспечивает их немедленное исполнение без дополнительных шагов.

bun index.ts

Разница в скорости объяснима отсутствием предварительной транспиляции, необходимой для Node.js.

Совместимость с ESM и CommonJS

В JavaScript популярно использование двух систем работы с модулями: CommonJS и ESM.

CommonJS основан на использовании require и module.exports для синхронной обработки модулей с возможностью динамического импорта.

ESM — более современная версия, ставшая стандартом с выходом ECMAScript 6, использует import/export. ESM поддерживает статичный импорт в начале файла, а также асинхронное взаимодействие с модулями с помощью ленивой загрузки. Это означает, что модули могут быть загружены и выполнены, только когда они действительно нужны.

NodeJS официально получил поддержку ESM начиная с версии 13.2.0. Bun поддерживает как CommonJS, так и ES-модули, позволяет использовать оба типа импортов в одном файле, но не требует дополнительной настройки. В Node нужно указать "type": "module” в package.json, если вы хотите использовать ESM, либо воспользоваться расширением .mjs

Работа с WebAPI

Браузерный WebAPI предоставляет удобные методы работы с сетью: fetch и WebSocket. Хотя это и стало стандартом в браузере, для работы в Node долгое время нужно было ставить дополнительные модули, например, node-fetch.

NodeJS получил поддержку fetch в экспериментальном режиме с релизом 18-й версии.

Bun же обеспечивает стабильную встроенную поддержку стандартных WebAPI: fetch, WebSocket и других. Собственная реализация этих WebAPI в Bun гарантирует, что они работают быстрее и надёжнее по сравнению с альтернативами.

async function fetchPosts() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();
	return posts;
}

fetchPosts();

Экспериментальная поддержка в Node 18 и старше. Стабильная поддержка Bun.

Горячая перезагрузка

Горячая перезагрузка, или hot reloading, — функция автоматического обновления или перезагрузки частей приложения в режиме реального времени, без необходимости полного перезапуска. Это неотъемлемая часть удобного опыта разработки.

В экосистеме Node есть несколько вариантов её реализации:

Использование nodemon, который полностью перезапускает процесс для обновления nodemon index.js

Node v18 получил экспериментальную поддержку флага --watchnode --watch index.js

Оба метода выполняют схожую функцию, но их работа может отличаться в зависимости от различных сред и сценариев. Например, nodemon может привести к сбоям, например, к разрыву соединений HTTP и WebSocket. Экспериментальный флаг --watch может не обладать рядом функций и имеет некоторые проблемы, описанные в issue на github.

****Bun значительно улучшает процесс разработки благодаря встроенной функции горячей перезагрузки. Для этого необходимо запустить Bun с флагом --hot: bun --hot index.ts

В отличие от методов Node, требующих полного перезапуска всего приложения, Bun перезагружает код на месте, не завершая старый процесс. Это гарантирует бесперебойность HTTP и WebSocket-соединений, а также сохранение состояния приложения. Это обеспечивает удобство разработки.

Обратная совместимость Bun с NodeJS

Bun позиционирует себя как замену для Node.js и стремится к 100% совместимости с существующими приложениями Node.js и npm-пакетами. Уже сейчас он поддерживает большинство npm-пакетов и встроенные библиотеки в Node:

  • fs, http, path и другие.
  • переменные __dirname, [__filename](<https://nodejs.org/api/globals.html#__filename>), process

Также команда Bun активно обрабатывает запросы, когда возникают проблемы с обратной совместимостью.

Работа с NodeJS API в Windows

С выходом версии v1.1  Bun получил полную поддержку Windows. С этих пор обе runtime-среды поддерживают все популярные серверные платформы. Однако благодаря низкоуровневым оптимизациям Bun скорость работы в Windows с Node API быстрее на 58%.

Также Bun стал кроссплатформенной оболочкой, в том числе и на Windows. Так, вы можете взаимодействовать с операционной системой посредством bun shell прямо из JavaScript:

import { $ } from "bun";

// pipe to stdout:
await $`ls *.js`;

// pipe to string:
const text = await $`ls *.js`.text();

Для реализации подобного функционала в Node вам потребовался бы модуль child_process.

const { exec } = require('child_process');

exec('ls *.js', (error, stdout, stderr) => {
    if (error) throw error;
    console.log(stdout); // результат выполнения команды ls *.js
});

Улучшенный API Bun чтения/записи по сравнению с Node.js

Bun предлагает оптимизированные API для работы с файлами, записи на диск и настройки HTTP-серверов — всё это значительно быстрее, чем в Node.js.

Bun.file()Позволяет получить доступ к файлам, реализуя ленивую загрузку. Этот метод работает до 10 раз быстрее, чем его аналог Node.js.

// Bun 
const file = Bun.file("package.json");
await file.text();

// Node.js
const fs = require("fs/promises");
const fileContents = await fs.readFile("package.json", "utf-8");

Bun.write() Реализует запись данных на диск. Может работать как со строками, так и с большими двоичными объектами (blob). Производит запись в 3 раза быстрее, чем аналог в Node.

// Bun
await Bun.write("index.html", "<html/>");

// Node.js
const fs = require("fs/promises");
await fs.writeFile("index.html", "<html/>");

Bun.serve()Позволяет установить http или websocket-сервер.

// Bun
Bun.serve({
    port: 3000,
    fetch(request) {
      return new Response("Bun!");
    },
  });
  
  // Node.js
  import http from "http";
  const server = http.createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Node.js!");
  });
  server.listen(3000);

Как утверждают разработчики Bun, http-сервер обрабатывает в 2,5 раза больше запросов в секунду, чем Node.js,. А также — в 7 раз больше сообщений по WebSocket, чем пакет ws в Node.js.

Но так ли это на самом деле? Для ответа на этот вопрос, я обратился к докладу Виктора Хомякова c HolyJS. Он утверждает, что бенчмарки проводимые командой Bun заведомо неверные:

  • Размеры заголовков и ответов сервера различаются. 202 байта в Bun, против 242 в Node.js
  • Различный механизм работы сервера – Bun использует uWebSockets (микро-вебсокеты) в реализации Bun.serve() о чем сообщает Jarred в обсуждении на GitHub
  • Обе сборки должны работать в production режиме, что означает минифицированный код без всего лишнего

При запуске бенчмарков в равных условиях тестирование RPS на Ubuntu с AMD EPYC дает нам одинаковый результат:

RPS CPU
Bun serve 48-50k 40%
Bun http 32-36k 60%
Node http 22-35k 100%
Node uWS 48-50k 40%

Bun против Node.js: менеджер пакетов

Bun также предоставляет пакетный менеджер — более быструю альтернативу, чем npm, yarn и pnpm. Как заявляют разработчики Bun, замена npm install на bun install ускорит установку ваших пакетов более чем в 25 раз.

Давайте сравним API Bun с аналогичным в npm:

Bun npm
bun install npm install Устанавливает зависимости из  package.json
bun add <package> npm install <package> Добавляет новую зависимость в проект
bun add <package> -D npm install <package> -D Добавляет новую зависимость для разработки
bun remove <package> npm uninstall <package> Удаляет пакет из проекта
bun update <package> npm update <package> Обновляет пакет до последней версии
bun run <script> npm run <script> Запускает скрипт из package.json

Скорость установки пакетов с помощью Bun превышает скорость установки через npm. Это происходит благодаря использованию глобального кэша модулей, исключая излишние загрузки из npm registry. Также Bun использует максимально быстрые системные вызовы, присутствующие в каждой системе, такие как хардлинки. Как следствие, Bun также экономит место на диске, так как все модули находятся в едином месте: ~/.bun/install/cache.

Из недостатков отметим, что вместо package-lock Bun использует свои бинарные файлы. Они предоставляют дополнительную оптимизацию, но ухудшают читабельность.

Bun как сборщик

Bun имеет встроенный сборщик, который превосходит современные решения, используемые для Node.js. В экосистеме Node.js процессы сборки, транспиляции, минификации кода реализуют комплексом инструментов. Bun же полностью контролирует сборку, реализуя её с помощью своей внутренней экосистемы.

Запустить билд можно командой:

bun build ./index.ts --outdir ./build

Сравним скорость с помощью теста  esbuild’s three.js benchmark.

Отличительной особенностью сборки Bun являются макросы. Макросы — это механизм запуска функций JavaScript во время сборки. Значение, возвращаемое этими функциями, напрямую встраивается в результат сборки.

Например, мы можем произвести некоторые вычисления, которые будут подставлены в результат сборки.

// whenever.ts
export function random() {
    return Math.random();
}

// cli.ts
import { random } from './random.ts' with { type: 'macro' };
console.log(`Your random number is ${random()}`);

Исходный код функции random отсутствует в финальном бандле. Вместо этого он исполнился во время сборки, и вызов функции random() был заменён результатом выполнения. Поскольку исходный код никогда не будет включён в пакет, макросы могут безопасно выполнять привилегированные операции (чтение из базы данных).

Примечание: Макросы обозначаются с использованием атрибутов импорта, что позволяет прикрепить дополнительные метаданные при импорте. Подробнее об этом предложении вы можете почитать на странице github.

Инструмент для тестирования Bun

Bun в ряде своих инструментов имеет среду для тестирования. Синтаксис похож на уже привычный нам Jest, что сильно упрощает миграцию.

test("Bun test", () => {
  expect('hello' + ' '+ 'world').toBe('hello world');
});

Производительность в тестах

Со слов разработчиков рантайм среды, в тестах Bun в 13 раз превосходит по скорости Jest, в 8 раз Vitest. Это достигнуто с помощью методов, имеющих низкоуровневую реализацию. Например, метод .toEqual() в Bun работает в 100 раз быстрее, чем Jest, и в 10 раз, чем Vitest.

Подводим итоги

  • Bun вызывает серьёзный интерес в сообществе.
  • Поддержка всех платформ уже на достаточно высоком уровне. All-in-one решение и  использование современных технологий делают его выигрышным выбором для повышения производительности JavaScript-приложений.
  • Однако, несмотря на бенчмарки и перфоманс-тесты, не стоит утверждать, что Node не может конкурировать с Bun.
  • В реальности синтетические тесты производительности Bun, как утверждает Виктор Хомяков, — липа и к ним стоит относиться скептически. Это объясняется тем, что тесты производительности гоняются на программах вида hello world в синтетически созданных, и часто неравных условиях. Это не может дать объективной оценки. На деле мы получаем сравнимо схожую производительность обеих систем.
  • Node.JS до сих пор является лидером по однопоточной обработке среди других серверных решений. И имеет большое комьюнити — значит, и большое число решённых проблем, что делает его более привлекательным для выбора в крупных компаниях, которым важна стабильность.
  • Bun предстоит пройти проверку временем. Тем не менее Bun — отличный инструмент, чтобы его начать использовать на новых небольших проектах.

©