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
loadContextis not being provided to thehandlerfunction 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.