home

Blog


Speeding up your React Router server-side-rendered (SSR) app with Hono


If you're running a React Router app in framework mode, you get SSR out of the box and for production it hands you @react-router/serve, a small server built on top of Express. While it does the job, Express carries a bit of history (and weight) with it, so if you're the kind of person who wants a leaner server, or just more control over what happens around your app, there's a really nice alternative that drops in with almost no glue.

Hono (and its Node server)

The createRequestHandler from react-router gives you a function that takes a standard web Request and resolves to a standard web Response, that's the whole contract. It doesn't care about Node, Express, or any particular server, it just wants a Request and it'll give you back a Response.

Hono is built on those exact same web standards and, inside its handler, context.req.raw is already a web Request. This means that passing a request from Hono to React Router is basically one line, with no conversions, no bridging and no fuss.

Since Hono itself doesn't listen on a port, it's just a handler, to actually run it on Node you'll have to bring in @hono/node-server, whose serve function takes your app.fetch and wires it up to Node's built-in http server.

Install both (npm i hono @hono/node-server) before this:

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { createRequestHandler, type ServerBuild } from "react-router";

const build = (await import("./build/server/index.js")) as ServerBuild; // This one here is the output of your bundle
const handler = createRequestHandler(build);

const app = new Hono();
app.use(async (context) => handler(context.req.raw));

serve({ fetch: app.fetch, port: 3000 });

That's it. serve spins up the Node HTTP server, every request gets turned into a web Request, Hono passes it to React Router, and the Response goes back out.

Squeezing the last bit of performance

If you must make every request count and you're not going to use Hono's ecosystem (middlewares and everything else), you can improve your performance by dropping the app entirely and relying only on a bare request listener.

import { createServer, type RequestListener } from "node:http";
import { getRequestListener } from "@hono/node-server";
import { createRequestHandler, type ServerBuild } from "react-router";

const build = (await import("./build/server/index.js")) as ServerBuild;
const handler = createRequestHandler(build);

const server = createServer(getRequestListener((request) => handler(request) as RequestListener));

server.listen({ port: 3000 });

Why not use Fastify?

Fastify is fast, mature, and plenty of people already reach for it by default. It's a great framework overall. Why not then?

The difference comes down to what each one speaks natively. Fastify is built on Node's own HTTP primitives, IncomingMessage and ServerResponse (you can reach them as request.raw and reply.raw). It doesn't hand you a web Request, and it doesn't want a web Response back. React Router deals in nothing but web Request and Response, so to marry the two, you would end up having to write an adapter to make it work.

Since you already have Node's own server available, you don't need to put Fastify in its place, because it would generate unnecessary overhead. Where Fastify really shines is when the server itself is the product: JSON APIs with schema-based validation and its blazing fast JSON stringify serialization. With that said, for an SSR app where your whole "API" is basically "give this web request to React Router and return the response", Hono removes an entire category of mess you'd have to maintain.

Before wrapping everything up, there are two things worth taking into consideration:

  • React Router's loadContext is not being provided to the handler function in the examples, so you'll need to write your own logic to provide it. I suggest taking a look at how @react-router/express, or some other library, does it;
  • There are a lot of things going on in your server when you call react-router-serve, that's why I also recommend taking a look at how it goes, so you don't miss anything you didn't know you needed.

Wrap-up

Swapping to Hono doesn't ask much of you. React Router already talks in web Request and Response, Hono does too, and @hono/node-server handles the Node part for you. You start with a handful of lines and from there you may add (or not) port handling, static serving, logging requests, etc. It's the performance boost your SSR app could be looking for.

Powered by Simple Blog API