JavaScript Development on Turbo: A Look at Bun and its Innovations

24.01.2024Ricky Elfner
Mobile JavaScript Frontend Framework Hands-on How-to

banner

This week, we’re diving into Bun, which was released in version 1.0 in September 2023. The developers describe Bun as an all-in-one toolkit for JavaScript and TypeScript applications.

The Bun Runtime

At the heart of the Bun runtime lies a fast JavaScript runtime, designed as a seamless alternative to Node.js. It’s written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory consumption. Zig is a modern, high-performance programming language chosen for its combination of stability, efficiency, and direct control over resources.

The Bun CLI Tool

The Bun command-line tool is additionally equipped with a test runner, script runner, and Node.js-compatible package manager. The big difference here is the significantly better performance compared to existing tools. Another major advantage is that Bun can be used in existing Node.js applications without major changes.

Faster Developer Experience than Node.js

Since the introduction of Node.js about 14 years ago, countless new features and tools have been added, which naturally caused Node.js to grow tremendously. This led to a certain complexity and size, which resulted in performance losses. The existing tools are very good in most cases, but since they are all available at once, this leads to a slow developer experience.

This is mainly due to the fact that the existing tools often lead to redundancies in their tasks. For example, if you run jest, the code is parsed at least three times. This is exactly where Bun wants to start and also convince with its speed, without losing the advantages of JavaScript.

Bun’s JavaScript Runtime

During development, the focus was primarily on speed. Especially with TypeScript files, the startup process of Bun alone is 4x faster than Node.js.

img

Image Source: bun.sh

Why is Bun so much faster?

This difference is mainly possible because it doesn’t use Google’s V8 engine, but Apple’s WebKit engine.

But also the support of TypeScript and JSX files was important. This is solved by Bun’s transpiler converting these files into conventional JavaScript before execution, without additional dependencies:

1
2
3
bun index.ts
bun index.jsx
bun index.tsx

Further support is of course also given and recommended for ES modules. Should CommonJS still be needed as with millions of packages on npm, this is also supported.

Bun supports import and require() in the same file

In Bun, there is the remarkable ability to use both import and require() within the same file – a flexibility that is not available by default in Node.js unless you use special features like the “Mixed Modules” feature.

Standard Web API Support

Bun implements standard Web APIs such as fetch, Request, Response, WebSocket, and ReadableStream. Bun is powered by the JavaScriptCore engine, which was developed by Apple for Safari. Therefore, some APIs like Headers and URL directly use Safari’s implementation.

Full Compatibility with Node.js Globals and Modules

Of course, Bun’s goal is full compatibility with Node.js built-in globals (process, Buffer) and modules (path, fs, http, etc.). However, it should be mentioned that this is an ongoing process that is not yet fully completed. The current status can be checked here: https://bun.sh/docs/runtime/nodejs-apis.

According to Bun, compatibility with existing frameworks should also be given. These include:

  • Next.js
  • Remix
  • Nuxt
  • Astro
  • SvelteKit
  • Nest
  • SolidStart
  • Vite

Hot-Reloading with Bun

Bun simplifies development work considerably. You can run Bun with the --hot parameter to enable hot reloading, which reloads your application when files are changed.

1
bun --hot server.ts

Unlike tools like nodemon, which hard restart the entire process, Bun reloads your code without terminating the old process. This means that HTTP and WebSocket connections are not disconnected and state is not lost. This approach is an advantage over common Node.js tools, which restart the entire process when changes are made to the code and therefore interrupt existing connections.

Extreme Flexibility with Bun Plugins

Bun is designed to be extremely customizable. With the ability to define plugins, you can intercept imports and perform custom loading operations. For example, a plugin could add support for additional file types such as yaml or png. The plugin API is inspired by esbuild, which means that most esbuild plugins can be used with Bun without any problems. This gives you maximum flexibility in designing your development processes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { plugin } from "bun";

plugin({
  name: "YAML",
  async setup(build) {
    const { load } = await import("js-yaml");
    const { readFileSync } = await import("fs");
    build.onLoad({ filter: /\.(yaml|yml)$/ }, (args) => {
      const text = readFileSync(args.path, "utf8");
      const exports = load(text) as Record<string, any>;
      return { exports, loader: "object" };
    });
  },
});

Bun APIs

Bun comes with top-optimized APIs as a standard library that offer exactly what you need most as a developer. Unlike the Node.js APIs, which are more intended for backward compatibility, these native Bun APIs are designed to be fast and intuitive to use.

Bun.file()

Bun goes one step further by returning a BunFile – an extension of the Web standard File. This file allows on-demand, lazy loading of content in various formats, which of course opens the door to a wide range of applications.

1
2
3
4
5
6
const file = Bun.file("package.json");

await file.text(); // string
await file.arrayBuffer(); // ArrayBuffer
await file.blob(); // Blob
await file.json(); // {...}

Bun.write()

With Bun, the versatile Bun.write() API simplifies writing a variety of data to disk. Whether it’s a string, binary data, blobs, or even a response object, these individual methods optimize the writing process. This flexibility greatly improves the developer experience as it is seamlessly possible to handle different data types without having to resort to multiple complex functions.

1
2
3
4
await Bun.write("index.html", "<html/>");
await Bun.write("index.html", Buffer.from("<html/>"));
await Bun.write("index.html", Bun.file("home.html"));
await Bun.write("index.html", await fetch("https://example.com/"));

Bun.serve()

Bun offers the ability to start an HTTP server, WebSocket server, or both with Bun.serve(). It uses familiar Web standard APIs such as Request and Response. Impressively, Bun can serve up to 4 times more requests per second than Node.js. This performance, combined with familiar APIs, makes Bun an efficient choice for deploying server applications while enabling seamless integration with common Web standards.

1
2
3
4
5
6
Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello from Bun!");
  },
});

Even TLS configurations can be implemented quickly and easily:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello from Bun!");
  },
  tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
  }
});

Bun makes supporting WebSockets alongside HTTP effortless. Simply define an event handler within websocket. This is in stark contrast to Node.js, which does not provide a built-in WebSocket API and relies on external dependencies such as ws. With Bun, implementing WebSockets into your application becomes as easy as native code, without the overhead of external libraries. In addition, Bun can serve up to 5 times more requests per second than Node.js.

1
2
3
4
5
6
7
8
Bun.serve({
  fetch() { ... },
  websocket: {
    open(ws) { ... },
    message(ws, data) { ... },
    close(ws, code, reason) { ... },
  },
});

bun:sqlite

Bun offers built-in support for SQLite with an API inspired by better-sqlite3, but written in native code to maximize speed. Compared to better-sqlite3 on Node.js, Bun allows queries to SQLite with up to 4 times the speed. This natively implemented support not only ensures fast and efficient database interaction, but also demonstrates Bun’s commitment to optimized performance and seamless integration of database functionality into the development process.

1
2
3
4
5
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Bun' as runtime;");
query.get(); // => { runtime: "Bun" }

Bun.password

In addition to its versatile plugin system, Bun also facilitates the implementation of common yet complex tasks that developers may not want to implement themselves from scratch.

With the Bun.password API, developers can effortlessly hash and verify passwords, all without external dependencies. This feature enables the use of recognized cryptographic algorithms such as bcrypt or argon2. By providing these ready-to-use solutions for essential but complex functions, Bun simplifies the development process and strengthens best security practices. Developers can thus focus on building robust and secure applications.

1
2
3
4
5
6
const password = "super-secure-pa$$word";
const hash = await Bun.password.hash(password);
// => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

const isMatch = await Bun.password.verify(password, hash);
// => true

The Package Manager

Even if Bun is not used as the primary runtime environment, Bun’s built-in package manager can significantly help increase the efficiency of your development workflow. No more long waits when installing dependencies that you may know from other package managers.

Bun is many times faster than npm, yarn, and pnpm. This is achieved by using a global module cache to avoid repeated downloads from the npm registry. It also uses the fastest system calls available on each operating system.

However, if you look at the commands, you will not see any difference to npm.

1
2
3
4
bun install
bun add <package> [--dev|--production|--peer]
bun remove <package>
bun update <package>

Even when running scripts using bun run, you save about 150 milliseconds each time

Script Runner ø Time
npm run 176ms
yarn run 131ms
pnpm run 259ms
bun run 7ms

The Test Runner

If you’ve already written tests in JavaScript, you’re probably familiar with Jest, which introduced the “expect"-style APIs.

Bun goes one step further with its built-in test module bun:test and is fully compatible with Jest. You can easily run your tests with the bun test command and benefit from all the advantages of the Bun runtime environment, including comprehensive support for TypeScript and JSX.

1
2
3
4
5
import { test, expect } from "bun:test";

test("2 + 2", () => {
  expect(2 + 2).toBe(4);
});

Migrating from Jest or Vitest to Bun is a breeze. Any imports from @jest/globals or vitest are automatically switched to bun:test, so everything works smoothly, even without code changes.

1
2
3
4
5
import { test } from "@jest/globals";

describe("test suite", () => {
  // ...
});

In a performance comparison with the test package for zod, Bun again comes out on top. It was a whopping 13 times faster than Jest and 8 times faster than Vitest. This performance boost is not just a number; it means more efficient and faster testing for your projects.

Zod is a powerful TypeScript library for defining data structures and validating them, allowing developers to create clear and secure schemas for their applications.

img

Image Source: bun.sh

Bun’s matchers (e.g. expect().toEqual()) are 100 times faster than in Jest and 10 times faster than in Vitest due to the implementation of native code.

There is also a direct implementation for GitHub Actions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: oven-sh/setup-bun@v1
      - run: bun test

This automatically adds annotations to the test errors so that your logs are easy to understand.

The Bundler

Bun stands out not only as a JavaScript and TypeScript bundler and minifier, but as a powerful solution to optimize and bundle your code for the browser, Node.js, and other platforms. Inspired by esbuild, Bun offers a well-designed plugin API that is highly efficient for both bundling and the runtime environment. Even the previous .yaml plugin is still ready to use to seamlessly integrate .yaml files during bundling.

In terms of speed, Bun naturally has the edge, according to esbuild’s benchmarks. It is 1.75 times faster than esbuild itself, 150 times faster than Parcel 2, 180 times faster than Rollup + Terser, and an impressive 220 times faster than Webpack. Thanks to the integrated runtime environment and bundler, Bun can perform functions that no other bundler can.

img

Image Source: bun.sh

JavaScript Macros

But that’s not all. Bun introduces the idea of JavaScript macros – small functions that optimize your code during bundling. The return values of these functions are inserted directly into your bundle. A typical use case would be, for example, reading a release version. This requires an index.ts file that calls another file release.ts. This happens as soon as bun build index.ts is executed.

1
2
3
4
5
import { getRelease } from "./release.ts" with { type: "macro" };

// The value of `release` is evaluated at bundle-time,
// and inlined into the bundle, not run-time.
const release = await getRelease();
1
2
3
4
5
6
7
export async function getRelease(): Promise<string> {
  const response = await fetch(
    "https://api.github.com/repos/oven-sh/bun/releases/latest"
  );
  const { tag_name } = await response.json();
  return tag_name;
}

Hands-On Examples

Next, I would like to show in a short hands-on example how to start developing with Bun.

Installation

I decided to do the installation simply via brew. However, there is also the possibility to do this via curl, npm, Docker or Proto.

1
2
brew tap oven-sh/bun # for macOS and Linux
brew install bun

Create Project

To have all the necessary files directly and to have the most important properties set, there is this command:

1
bun init

This will provide the following structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.
├── README.md
├── bun.lockb
├── index.ts
├── node_modules
│   ├── bun-types
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── types.d.ts
│   └── typescript
│       ├── LICENSE.txt
│       ├── README.md
│       ├── SECURITY.md
│       ├── ThirdPartyNoticeText.txt
│       ├── bin
│       ├── lib
│       └── package.json
├── package.json
└── tsconfig.json

Create Server

Within the index.ts file, an endpoint is now provided via port 3000 as soon as bun run index.ts is executed. This will return the simple text "Hello b-nova" by calling http://localhost:3000.

1
2
3
4
5
6
7
8
9
//create server
const server = Bun.serve({
    port: 3000,
    fetch(req){
        return new Response("Hello b-nova")
    }
})

console.log(`Listening on PORT http://localhost:${server.port}`)

In the next step, we want to process the output a bit. For this example, we need the two packages figlet and @types/figlet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bun add figlet

Output:
bun add v1.0.14 (d8be3e51)

 installed figlet@1.7.0 with binaries:
  - figlet


 1 package installed [954.00ms]
1
bun add @types/figlet

Once these are installed, they can be imported and used as follows:

1
2
3
4
5
6
7
8
9
import figlet from "figlet";

const server = Bun.serve({
    port: 3000,
    fetch(req){
        const body = figlet.textSync("Hello b-nova")
        return new Response(body)
    }
})

This creates a revised output: image-20231201105317202

As a final step, we now want to create different routers within our server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import figlet from "figlet";

const server = Bun.serve({
    port: 3000,
    fetch(req){
        var body = ''
        const url = new URL(req.url)

        if(url.pathname === '/'){
            body = figlet.textSync("Hello all")
        }
        if (url.pathname === '/b-nova'){
            body = figlet.textSync("Hello b-nova")
        }
        
        return new Response(body)
    }
})

Conclusion

Overall, Bun presents itself as an impressive toolkit for JavaScript and TypeScript, shining with innovative features such as lightning-fast bundling, Jest compatibility in testing, and performance gains through native implementations.

The well-thought-out integration of runtime environment and bundler, as well as the introduction of JavaScript macros, make Bun a modern solution that enables developers to write efficient and performant code. With its speed and flexibility, Bun offers a promising platform for developers to thrive in the ever-evolving JavaScript world. From our point of view, a must-try! 🚀

This techup has been translated automatically by Gemini

Ricky Elfner

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.