On September 8th, there was fresh buzz in the JavaScript community: Bun v1.0, created by Jarred Sumner, had arrived. But with all the talk, many are left wondering: What's the essence of Bun? Why is everyone drawing parallels with the tried-and-true Node.js? Is Bun just another fleeting trend, or is it here to redefine the game? In this article, let’s dive deep into Bun, check out its features, and find out how it compares to the well-established Node.js.
What is Bun?
Bun is a super fast all-in-one toolkit for JavaScript and TypeScript apps. The beauty of Bun lies in its ability to streamline the development process, making it smoother and more efficient than ever. This is possible because Bun is not just a runtime, it's also a package manager, a bundler, and a test runner. Imagine having a Swiss Army knife for JS development; that's Bun for you.
What Bun solves for
The inception of Node.js in 2009 was groundbreaking. However, as with many technologies, as it grew, so did its complexity. Think of it like a city. As a city expands, traffic congestion can become a problem.
Bun aims to be the new infrastructure that alleviates this congestion, making things run smoother and faster. It's not about reinventing the wheel but refining it, ensuring that while we get the speed and simplicity, we don't lose the essence of what makes JavaScript unique and powerful.
Bun is designed as a faster, leaner, more modern replacement for Node.js so let’s take a closer look at some comparison. But first let’s discuss one other topic.
Node.js versus Deno versus Bun
When discussing the evolution of JavaScript runtimes, it's hard to overlook Deno. Ryan Dahl, the creator of Node.js, introduced Deno as a new runtime that aimed to address some of the challenges and regrets he identified with Node.js.
Deno is a secure runtime for JavaScript and TypeScript. It directly addresses many of the shortcomings of Node.js. For instance, Deno natively supports TypeScript without the need for external tools. Unlike Node.js, where scripts have broad permissions by default, Deno adopts a security-first approach requiring developers to explicitly grant permissions for potentially sensitive operations, such as file system access or network connectivity.
While Deno presents a compelling alternative to Node.js, it hasn't matched Node.js's widespread adoption. Therefore, this article centers on contrasting Bun with the well-established Node.js.
Getting started
With Bun, we can scaffold an empty project with the command bun init -y
. We have a few files generated and in index.ts, add a line, console.log("Hello, Bun!")
. In the terminal, run the command bun index.ts
to see “Hello, Bun!” logged.
Bun versus Node.js: JavaScript runtime
A JavaScript runtime is an environment which provides all the necessary components in order to use and run a JavaScript program.
Both Node.js and Bun are runtimes. Node.js is primarily written in C++ where as Bun is written with a low-level general purpose programming language called Zig. But that is just the tip of the ice berg. Let’s take a closer look at other differences when treating Bun as a runtime alone.
JavaScript engine
A JavaScript engine is a program that converts JavaScript code we write into machine code that allows a computer to perform specific tasks.
While Node.js uses Google's V8 engine that power's Chrome browser, Bun uses JavaScriptCore (JSC), which is an open source JavaScript engine developed by Apple for Safari.
V8 and JSC have different architectures and optimization strategies. JSC prioritizes faster start times and reduced memory usage with a slightly slower execution time. On the other hand, V8 prioritizes fast execution with more runtime optimization which may lead to more memory usage.
This makes Bun fast, starting up to 4x faster than Node.js.
Summary: bun ran 2.19 times faster than deno and 4.81 times faster than node
Transpiler
While Node.js is a powerful runtime for JavaScript, it doesn't natively support TypeScript files. To execute TypeScript in a Node.js environment, external dependencies are required. One common approach is to use a build step to transpile TypeScript (TS) into JavaScript (JS) and then run the resulting JS code. Here's a basic setup that uses the ts-node
package:
1. Installation
npm install -D typescript ts-node
2. Script configuration
In your package.json
, you can set up scripts to streamline the process:
{
"scripts": {
"start": "ts-node ./path/to/your/file.ts"
}
}
3. Execution
With the above script, you can easily run your TypeScript file:
npm start
In contrast, Bun offers a more streamlined approach. It comes with a JavaScript transpiler integrated into the runtime. This allows you to directly run .js, .ts, .jsx
and .tsx
files. Bun's built-in transpiler seamlessly converts these files to vanilla JavaScript, facilitating immediate execution without additional steps.
bun index.ts
The difference in speed is magnified when running a TypeScript file as Node.js requires a transpilation step before it can be run.
ESM and CommonJS compatibility
A module system allows developers to organize code into reusable segments. In JavaScript, the two primary module systems are CommonJS and ES modules (ESM). CommonJS, originating from Node.js, uses require
and module.exports
for synchronous module handling, ideal for server-side operations.
ESM, introduced in ES6, employs import
and export
statements, providing a more static and asynchronous approach, optimized for browsers and modern build tools. Let's use colors
for CommonJS and chalk
for ESM, two popular packages for adding colored outputs to the console and to better understand the module systems.
Node.js has been traditionally associated with the CommonJS module system. Here's a typical usage:
// CommonJS in Node.js (index.js)
const colors = require("colors");
console.log(colors.green('Hello, world!'));
For ES modules in Node.js, you have one of two options:
- You need to include
"type": "module"
in yourpackage.json
. - Use the
.mjs
extension.
// ESM in Node.js (index.mjs)
import chalk from 'chalk';
console.log(chalk.blue('Hello, world!'));
The transition from CommonJS to ES modules (ESM) has been a complex journey. It took Node.js half a decade post ESM's introduction to support it without the experimental flag. Still, CommonJS remains prevalent in the ecosystem.
Bun simplifies the module system by supporting both without any special configuration. Bun's standout feature is its ability to support both import
and require()
in the same file, something not natively possible in Node.js:
// Mixed modules in Bun (index.js)
import chalk from "chalk";
const colors = require("colors");
console.log(chalk.magenta('Hello from chalk!'));
console.log(colors.cyan('Hello from colors!'));
Web APIs
Web APIs, integral to browser-based applications, offer tools like fetch
and WebSocket
for web interactions. While these have become browser standards, their support in server-side environments like Node.js has been inconsistent.
In earlier versions of Node.js, Web standard APIs commonly available in browsers weren't natively supported. Developers had to rely on third-party packages like node-fetch
to replicate this functionality. However, from Node.js v18, there's experimental support for the fetch
API, potentially eliminating the need for these packages.
Bun simplifies this by offering built-in support for these Web standard APIs. Developers can directly use stable fetch
, Request
, Response
, WebSocket
and other browser-like APIs without any additional packages. Furthermore, Bun's native implementation of these Web APIs ensures they are faster and more reliable compared to third-party alternatives.
Here's an example compatible with both Node.js (v18 and above) and Bun. While it's experimental in Node.js, the same functionality is stable in Bun:
// Experiment fetch in Node.js (v18 and above) and built-in fetch in Bun
async function fetchUserData() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await response.json();
console.log(user.name);
}
fetchUserData(); // Leanne Graham
Hot reloading
Hot reloading is a feature that boosts developer productivity by automatically refreshing or reloading parts of an application in real-time as the code changes, without requiring a full restart.
In the Node.js ecosystem, you have a couple of options for achieving hot reloading. One popular tool has been nodemon
, which hard-restarts the entire process:
nodemon index.js
Alternatively, starting from Node.js v18, there's an experimental --watch
flag introduced:
node --watch index.js
Both methods aim to provide real-time reloading of the application as code changes. However, they might have different behaviors, especially in certain environments or scenarios.
For instance, nodemon
can lead to disruptions like disconnecting HTTP and WebSocket connections, while the --watch
flag, being experimental, might not offer the full suite of features and has some reported issues in the GitHub issues.
Bun takes hot reloading a step further. By running Bun with the --hot
flag, hot reloading is enabled:
bun --hot index.ts
Unlike the Node.js methods that might require a full process restart, Bun reloads your code in-place without terminating the old process. This ensures that HTTP and WebSocket connections remain uninterrupted and the application state is preserved, providing a smoother development experience.
Node.js compatibility
When transitioning to a new runtime or environment, compatibility is often a primary concern for developers. Bun addresses this by positioning itself as a drop-in replacement for Node.js. This means that existing Node.js applications and npm packages can seamlessly integrate with Bun without any modifications. Key features that ensure this compatibility include:
- Support for built-in Node.js modules such as
fs
,path
, andnet
. - Recognition of global variables like
__dirname
andprocess
. - Adherence to the Node.js module resolution algorithm, including the familiar
node_modules
structure.
Bun is still evolving. It's tailored to enhance development workflows and is ideal for environments where resources are limited, such as serverless functions. The team behind Bun is striving for comprehensive Node.js compatibility and better integration with prevalent frameworks
While Bun ensures compatibility with Node.js, it doesn't stop there. Bun ships with highly-optimized, standard-library APIs for the things you need most as a developer.
Bun APIs
Bun.file()
Lazily load files and access their content in various formats. This method is up to 10x faster than its Node.js counterpart.
// Bun (index.ts)
const file = Bun.file("package.json");
await file.text();
// Node.js (index.mjs)
const fs = require("fs/promises");
const fileContents = await fs.readFile("package.json", "utf-8");
Bun.write()
A versatile API to write data to disk, from strings to Blobs. It writes up to 3x faster than Node.js.
// Bun (index.ts)
await Bun.write("index.html", "<html/>");
// Node.js (index.mjs)
const fs = require("fs/promises");
await fs.writeFile("index.html", "<html/>");
Bun.serve()
Set up HTTP server or WebSocket server using Web-standard APIs. It can serve 4x more requests per second than Node.js and handle 5x more WebSocket messages than the ws
package in Node.js. This backend capability is reminiscent of how developers use Express in Node.js but with the added benefits of Bun's performance optimizations.
// Bun (index.ts)
Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
});
// Node.js (index.mjs)
import http from "http";
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from Node.js!");
});
server.listen(3000);
Bun also has support for sqlite and password built-in.
Bun versus Node.js: package manager
Bun is more than just a runtime; it's an advanced toolkit that includes a powerful package manager. If you've ever found yourself patiently waiting during dependency installations, Bun offers a refreshingly faster alternative. Even if you don't use Bun as a runtime, its built-in package manager can speed up your development workflow.
Check out this table comparing Bun commands with npm, Node's package manager:
Bun | npm | Purpose |
---|---|---|
|
| Install all dependencies from |
|
| Add a new package to the project |
|
| Add a new development-only package |
|
| Remove a package from the project |
|
| Update a specific package to its latest version |
|
| Execute a specified script from |
At a glance, Bun's commands might seem familiar but the experience is anything but ordinary. Bun boasts installation speeds that are orders of magnitude faster than npm. It achieves this by leveraging a global module cache, eliminating redundant downloads from the npm registry. Additionally, Bun employs the fastest system calls available for each operating system, ensuring optimal performance.
Here's a speed comparison of installing dependencies for a starter Remix project from cache, comparing Bun and npm:
The bun
CLI contains a Node.js-compatible package manager designed to be a dramatically faster replacement for npm
, yarn
, and pnpm
Moreover, a bun run <command>
takes just 7ms, while npm run <command>
takes 176ms. While Node.js's npm has been the standard for JavaScript package management for years, Bun really is a speed powerhouse and presents a compelling alternative.
Bun versus Node.js: bundler
Bundling is the process of taking multiple JavaScript files and merging them into one or more optimized bundles. This process can also involve transformations, such as converting TypeScript to JavaScript or minifying the code to reduce its size.
In the Node.js ecosystem, bundling is typically handled by third-party tools rather than Node.js itself. Some of the most popular bundlers in the Node.js world include Webpack, Rollup, and Parcel, offering features like code splitting, tree shaking, and hot module replacement.
Bun, on the other hand, is not just a runtime and a package manager but also a bundler in its own right. It's designed to bundle JavaScript and TypeScript code for various platforms, including frontend applications in the browser (React or Next.js applications) and Node.js.
To bundle with Bun, you can use a simple command:
bun build ./index.ts --outdir ./build
This command bundles the index.ts
file and outputs the result in the ./build
directory. The bundling process is incredibly fast, with Bun being 1.75x faster than esbuild, and significantly outpacing other bundlers like Parcel and Webpack.
Bun takes 0.17s, esbuild 0.3s, rspack 4.45s, Parcel 2 26.32s, Rollup 32s and Webpack 5 38.02s
A standout feature in Bun is its introduction of JavaScript macros. These allow for the execution of JavaScript functions during bundling, with the results directly inlined into the final bundle. This mechanism offers a fresh perspective on bundling.
Check out this example where Bun's JavaScript macros are leveraged to fetch a username during the bundling process. Instead of making a runtime API call, the macro fetches the data at bundle-time, inlining the result directly into the final output:
// users.ts
export async function getUsername() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await response.json();
return user.name;
}
// index.ts
import { getUsername } from "./users.ts" with { type: "macro" };
const username = await getUsername();
// build/index.js
var user = await "Leanne Graham";
console.log(user);
While Node.js has its established bundling tools, Bun offers an integrated, faster, and innovative alternative that could reshape the bundling landscape.
Bun versus Node.js: test runner
Testing is a crucial aspect of software development, which ensures that code behaves as expected and catches potential issues before they reach production. In addition to being a runtime, a package manager and a bundler, Bun is also a test runner.
While Node.js developers have traditionally relied on Jest for their testing needs, Bun introduces a built-in test runner that promises speed, compatibility, and a range of features that cater to modern development workflows.
Bun's test runner, bun:test
, is designed to be fully compatible with Jest, a testing framework known for its "expect"-style APIs. This compatibility ensures that developers familiar with Jest can easily transition to Bun without a steep learning curve.
import { test, expect } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
Executing tests is straightforward with the bun test
command. Moreover, Bun's runtime supports TypeScript and JSX out of the box, eliminating the need for additional configurations or plugins.
Migrating from Jest or Vitest
Bun's commitment to compatibility shines through its support for Jest's global imports. For instance, importing from @jest/globals
or vitest
will be internally re-mapped to bun:test
. This means that existing test suites can run on Bun without any code modifications.
// index.test.ts
import { test } from "@jest/globals";
describe("test suite", () => {
test("addition", () => {
expect(1 + 1).toBe(2);
});
});
Performance benchmarks
Bun's test runner is not just about compatibility; it's about speed. In a benchmark against the test suite for Zod, Bun proved to be 13x faster than Jest and 8x faster than Vitest. This speed advantage is further highlighted by Bun's matchers, which are implemented in fast native code. For instance, expect().toEqual()
in Bun is a staggering 100x faster than Jest and 10x faster than Vitest.
Whether you're looking to migrate existing tests or start a new project, Bun provides a robust testing environment that aligns with modern development needs.
Conclusion
Node.js has long been a cornerstone in the JavaScript world, setting benchmarks and guiding developers. However, Bun is stepping onto the scene as a noteworthy challenger, pushing boundaries.
While it's still early days for Bun, the buzz it's generating is undeniable. Currently, it's optimized for MacOS and Linux, and while Windows support is in progress, some features are still on the horizon. With all it offers, Bun is certainly a toolkit you should consider exploring.