Bun vs Node with Nest.js

The latest potential replacement for Node recently dropped version 1.0 over at bun.sh. I played with it for a day, and I have some thoughts to share. I'll also provide an example you can play with in a GitHub CodeSpace very easily. In short, this isn't likely going to replace Node for you today, but keep an eye on it in the future.

What is Bun?

Bun attempts to combine a number of tools into a single tool, promising a consistent, efficient stack.

  • Runtime - Replace Node.js with a runtime based on JavaScriptCore (Apple) instead of V8 (Google)

  • Package Manager - Replace npm with a faster package manager with a binary lock file format

  • Bundler - Replace the tsc TypeScript compiler as well as esbuild or webpack

  • Test runner - Almost drop-in replacement for jest. It runs tests and also has built-in mocks and assertions.

  • The Node API - most Node built-ins like fs and crypto are compatible, but also it adds a bunch of "friendlier" functions.

That's a lot.

Managing a tool stack is always a challenge for developers. It can be overwhelming when we have to learn 15 different tools to manage even a small project. To manage even a basic React project, you might need a basic understanding of all of these: Typescript, Node and npm (local development, even for a SPA), eslint, webpack, and React itself (which uses jsx, a sub-language of its own). In a previous post, I discussed using jest vs a combination of mocha, sinon, TypeMoq, and should/chai. It can be dizzying. It's understandable that we want to combine those things into a smaller list of things to learn and memorize.

On the other hand, remember the idiom: "Jack of all trades, master of none." Usually an all-in-one tool comes with several compromises. In that previous post, I advocated for Jest, but I'll readily admit that it doesn't do mocks as well as TypeMoq, it doesn't do assertions as well as chai, and mocha is a more flexible test runner.

We bridge this gap by using opinionated frameworks. Nest.js (backend) and Next.js (full-stack React) solve this by choosing a set of tools for you and building defaults around those tools.

In this case, I found that Bun is a "master of none" or maybe a "master of one" as the saying sometimes goes.

Here's the example

Clone this repo, or open it in a GitHub Codespace. A Codespace will be the fastest way to see some results.

The package manager

bun install is blazingly fast. It's so fast you'll think something went wrong. For me, it's a reduction from 40s to 8s.

There is a downside. It uses a special binary lock file format that is not human-readable. You can use bun install -y to generate a yarn.lock file that is human-readable. You should commit lock files and being able to see the diff between versions of a lock file can be important.

The test runner

The test runner is blazingly fast.

Compared to the same tests with Jest:

The above tests were run on the same files. For the most part, Bun was completely compatible with Jest, but it wasn't perfect. I use jest-mock-extended which depends on certain things like jest.fn being available globally. Per the Bun docs, this should work, but I had to add a line global.jest = jest; to get jest-mock-extended to work. There might be a better way, but was able to prove my point.

There's a quirk in the coverage listed above. The jest tests showed 100% coverage on encryption-generator.ts, but Bun shows 80% coverage of functions. That doesn't make sense, since 100% of the lines are covered.

This is a big win, but I'm not ready to ditch Jest for it yet.

Runtime performance

Bun advertises that it's faster.

This graphic on their home page is very attractive. I can serve 4x the amount of traffic with the same resources?

My benchmark test is simple. The /crypt/<times> route will run an encryption function a given number of times. This tends to be computationally expensive, so I assumed that this would be a good test of the runtime.

1000 consecutive requests where each request runs the "encrypt" function 100 times:

bun startbun dist/main.jsnode dist/main.js
3875ms5977ms3897ms

1000 concurrent requests where each request runs the "encrypt" function 100 times

bun startbun dist/main.jsnode dist/main.js
3159ms4265ms3301ms

Bun isn't performing better than Node 20 in these tests. The big surprise is that Bun is faster at running Typescript directly (bun start) than it is running the compiled JS files. I even set "target": "ES2022" to get the most efficient compiled target for Node 20. Very big surprise that running TS files directly is faster. But also, it's not really faster than running Node 20 on the compiled JS files.

Why is Bun advertising faster benchmarks?

Bun is comparing it's special built-in HTTP server Bun.serve against Node's http.createServer (see test). Bun's main claims to speed are about their special built-in libraries, not the Javascript runtime.

Conclusion

A very fast test runner with some quirks. A very fast package manager with an unreadable lock file. These are interesting, but probably not enough of a benefit to dive into a new tool stack.

Bun.serve is the biggest benefit here. But generally, I don't use http.createServer, I use Express or Nest.js. So I might be able to wait for Nest or Express to support Bun.serve.

I only spent about a day on this. I decided pretty quickly that I didn't want to switch anything big to it right now. The single-tool aspect didn't drastically change my workflow. But it is an interesting tool to keep an eye on. Faster installs, tests, and execution in the future is an exciting promise.

Should I try this again from a different angle? Feedback welcome.