Релиз 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:
На самом деле такой тест не говорит о превосходстве в скорости 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:
Установка 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, 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 — отличный инструмент, чтобы его начать использовать на новых небольших проектах.
Комментарии