Node.js vs Deno vs Bun: serving JS files performance comparison

Mayank C
Tech Tonic

--

I’ve done a good number of performance comparisons in the past 2 years. All of those comparisons were good & valid at that time. However, things are changing fast these days. New technologies are coming in (like Bun), and continual better performance is coming out (recent improvements in Deno and Node.js). Going with the fast changing world, the performance comparisons also need redoing to stay relevant. Any comparisons done a year back may be totally outdated now.

In other articles, I’ve compared Node.js, Deno, and Bun for:

In this article, I’m going to compare Node.js, Deno, and Bun (No Go this time) when serving JS files (aka file server). This is relooking at the performance with the recent releases in all the technologies. Let’s see if anything changes this time.

Test Setup

All the tests are executed on MacBook M1 with 16G RAM. The HTTP tester is the well known benchmarking tool called Bombardier. Bombardier is a HTTP(S) benchmarking tool. It is written in Go programming language and uses excellent fasthttp instead of Go’s default net/http library, because of its lightning fast performance. The tester runs the tests with the following levels of concurrency:

  • 10 concurrent connections
  • 50 concurrent connections
  • 100 concurrent connections
  • 300 concurrent connections

The software versions are:

  • Node.js v19.5.0
  • Deno v1.30.0
  • Bun v0.5.1

A total of 1M (1 million file serves) requests are executed in each test.

The JS used for serving is a locally downloaded copy of the minified jquery 3.6.3 taken from https://code.jquery.com/jquery-3.6.3.min.js. The file size is ~88K.

Code

Only native HTTP file servers are used in this comparison. No third-party packages or libraries are used.

Node.js

import { createReadStream } from "node:fs";
import http from "node:http";
const basePath = "./data";

http.createServer((req, resp) => {
resp.setHeader("content-type", "text/javascript");
resp.writeHead(200);
const fp = basePath + req.url;
const rs = createReadStream(fp);
rs.on("open", () => {
rs.pipe(resp);
});
rs.on("error", () => {
resp.writeHead(404, "Not Found");
resp.end();
});
}).listen(3000);

Deno

const basePath = "./data";

Deno.serve(async (request: Request) => {
const fp = basePath + new URL(request.url).pathname;
try {
const file = await Deno.open(fp);
return new Response(file.readable, {
headers: {
"content-type": "text/javascript",
},
});
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response(null, { status: 404 });
}
return new Response(null, { status: 500 });
}
}, { port: 3000 });

Bun

const basePath = "./data";
Bun.serve({
port: 3000,
fetch(request) {
const fp = basePath + new URL(request.url).pathname;
try {
return new Response(Bun.file(fp));
} catch (e) {
return new Response(null, { status: 404 });
}
},
});

Bun sets the content-type and content-length headers internally.

Measurements

It’s not only about how fast a system is. A system/runtime can be very fast, but may stall the system by using too much CPU, memory, disk, etc. Some official benchmarks only focus on RPS. The more RPS, the better. Given unlimited resources, this is probably fine. However, for all practical purposes, only RPS doesn’t represent the complete picture. We need to measure latencies, and system usage too. Also, we need to see the response time distribution across different quantiles.

Here is the complete list of measurements:

  • 1st quartile (or 25th percentile) latency
  • Mean latency
  • Median latency
  • 3rd quartile (or 75th percentile) latency
  • 90th percentile latency
  • Maximum latency
  • Avg. CPU usage
  • Avg. Memory usage

Results

The following are the charts to show the results. There is one bar chart for each measurement metric. All concurrency levels are covered for each metric in the same chart.

All latencies are in microseconds

My analysis

Just like the comparison for serving images, Node.js continues to be the slowest of all.

This time, Deno beats Bun by serving JS files faster. Deno can serve 1M JS files in 27 seconds, while Bun needs 32 seconds.

However, Deno’s performance comes at a high price. Deno’s CPU and memory usage is higher than Bun’s. For 300 concurrent connections, Deno uses 247% CPU, while Bun uses 98%. For the same concurrency, Deno uses 87M memory, while Bun uses 29M.

--

--