Читать первым в Telegram-канале «Код Дурова»
Содержание
Релиз 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 — отличный инструмент, чтобы его начать использовать на новых небольших проектах.