Node.js vs Deno vs Bun: Server-side rendering 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.

The other article in the performance comparison series are:

In this article, I’m going to test SSR performance for a simple hello world use case.

NOTE: The last published version of React is 18.2.0 that got released in June 2022. The latest react code has optimizations for Bun. However, since this is not yet officially available, the base react 18.2.0 is used for both Node.js and Bun.

SSR

Server-side rendering (SSR) is an application’s ability to convert HTML files on the server into a fully rendered HTML page for the client. The web browser submits a request for information from the server, which instantly responds by sending a fully rendered page to the client. Search engines can crawl and index content prior to delivery, which is beneficial for Search Engine Optimization purposes.

Popular examples of server-side rendering JavaScript frameworks include: Angular server side rendering, ejs server side rendering, server side rendering Express, Gatsby server side rendering, Google server side rendering, NestJS server side rendering, Next server side rendering, Nuxt server side rendering, React server side rendering, and Vue server side rendering.

More details are present on the https://en.wikipedia.org/wiki/Server-side_scripting.

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) requests are executed in each test.

Code

I’ve used NPM’s standard react and react-dom package for SSR. This is different from Bun’s benchmark’s packages, where Bun uses some local copy of the React framework (https://github.com/oven-sh/bun/blob/main/bench/react-hello-world/react-hello-world.jsx, https://github.com/oven-sh/bun/blob/main/test/bun.js/react-dom-server.bun.cjs). I’m using NPM package because that’s what is available in market. The latest React code on GitHub has Bun’s optimizations, but this hasn’t been released through NPM. https://github.com/facebook/react/blob/main/packages/react-dom/server.bun.js.

For Bun, and Node.js, the same package is used. While for Deno, the package is imported directly from a CDN URL.

Another important difference is that, only Node.js needs the build step. Bun and Deno doesn’t need an explicit build step. For building the React app in Node.js, I’ve used the esbuild package.

> npm install react react-dom
> npm install esbuild

A snippet from package.json is:

"scripts": {
"build:server": "esbuild node_ssr_hello_world.jsx --outfile=node_ssr_hello_world.mjs --platform=node",
"start": "node node_ssr_hello_world.mjs",
"build": "npm run build:server"
}

The build log for Node.js is as follows:

> npm run build

> build
> npm run build:server


> build:server
> esbuild node_ssr_hello_world.jsx --outfile=node_ssr_hello_world.mjs --platform=node


node_ssr_hello_world.mjs 1013b

⚡ Done in 6ms

As mentioned earlier, there is no build step for Bun and Deno. Both can run the JSX and TSX files directly.

For Bun, instead of bun run, bun — jsx-production is used (as recommended by Bun’s benchmark script):

> bun --jsx-production bun_ssr_hello_world.jsx

I don’t know what’s special about — jsx-production option.

The app is a simple hello world SSR app.

Node.js

import { renderToPipeableStream } from "react-dom/server.node";
import React from "react";
import http from "http";
const App = () => (
<html>
<body>
<h1>Hello World</h1>
<p>This is an example.</p>
</body>
</html>
);
var didError = false;
http
.createServer(function (req, res) {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = didError ? 500 : 200;
res.setHeader("Content-type", "text/html");
res.setHeader("Cache-Control", "no-transform");
stream.pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send(
'<!doctype html><p>Loading...</p><script src="clientrender.js"></script>',
);
},
onAllReady() {
},
onError(err) {
didError = true;
console.error(err);
},
});
})
.listen(3000);

Deno

import { renderToReadableStream } from "https://esm.run/react-dom/server";
import * as React from "https://esm.run/react";

const App = () => (
<html>
<body>
<h1>Hello World</h1>
<p>This is an example.</p>
</body>
</html>
);

const headers = {
headers: {
"Content-Type": "text/html",
"Cache-Control": "no-transform",
},
};

Deno.serve(
async (req) => {
return new Response(await renderToReadableStream(<App />), headers);
},
{ port: 3000 },
);

Bun

import { renderToReadableStream } from "react-dom/server";
const headers = {
headers: {
"Content-Type": "text/html",
},
};

const App = () => (
<html>
<body>
<h1>Hello World</h1>
<p>This is an example.</p>
</body>
</html>
);

Bun.serve({
port: 3000,
async fetch(req) {
return new Response(await renderToReadableStream(<App />), headers);
},
});

Bun and Deno’s hello world SSR code looks much cleaner than Node’s.

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.

My analysis

The results are surprising. Bun and Node.js performs almost the same. Deno performs way better than both of them. I didn’t expect my results to be so different from Bun’s official benchmarks. The main reason could be the use of the standard react instead of Bun’s optimized react.

Overall, Deno offers the best SSR performance using almost the same CPU and memory.

At the end, Bun didn’t turn out to be three or four times faster. Again, this could be due to using unoptimized code, but that’s how it is right now. As mentioned earlier, I’ll update this article as soon as I see that React has published a new release.

--

--