Remember the SSR Performance Showdown? And Theo's coverage of it?

Now that @fastify/vue and @fastify/react reached 1.0.0, it's time for a revisit.

Especially because we didn't test metaframeworks back then. We were just looking at the raw performance of frontend frameworks.

Now, how do Vue and React fare when used in Fastify, Nuxt and Next?

Let's find out.

Creating the Next version

For the Next version, I just used create-next-app with most default settings, the App Router, and then copied over the spiral code from the vanilla React benchmark. I also had to make the HTML shell a React component:

export default function RootLayout ({ children }) {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

And finally, I had to export dynamic set to force-dynamic from my route module, otherwise Next will perhaps correctly identify it as a static component, and prerender it at build time. This is necessary to ensure even a simple component like this gets rendered dynamically every time:

export const dynamic = 'force-dynamic'

export default function Home () {
  const wrapperWidth = 960
  const wrapperHeight = 720
  const cellSize = 10
  const centerX = wrapperWidth / 2
  const centerY = wrapperHeight / 2

Creating the @fastify/react version

For the @fastify/react version, all I had to do was copy over the original code from the vanilla React example and update server.js and vite.config.js to use @fastify/react. If you compare these folders, you'll see no other change.

Note that in the original version, @fastify/vite was used directly. In this version we're using @fastify/vite with @fastify/react, which is just a renderer package for the former. The only difference is that @fastify/react, like Next, bundles an application shell some framework-y features.

Creating the Nuxt version

For the Nuxt version, like the Next version, I just used create-nuxt-app with most default settings. Then again, I copied over the code from the original Vue example and I also had to turn the HTML shell into a Vue component:

<template>
  <Head>
    <Meta charset="utf-8" />
    <Meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <Style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #f0f0f0;
      margin: 0;
    }

Note how you have to use Nuxt's special <Head>, <Meta> and <Style> components, which presumably use useHead() under the hood.

Creating the @fastify/vue version

For the @fastify/vue version, I followed the same steps as I did for the @fastify/react version, copying over the spiral code from the original Vue example and updating server.js and vite.config.js to use @fastify/vue.

Results

This is what I'm seeing:

  • @fastify/vue: 717 requests per second
  • Nuxt: 561 requests per second
  • @fastify/react: 347 requests per second
  • Next: 49 requests per second

Which basically means @fastify/react is 7x faster than Next.js.

I am, however, a bit surprised with that result. What is it this time? Next automatically sets NODE_ENV to production, so that can't be it. Where is the overhead coming from? Could it be the App Router and its requirement of having a React component as HTML shell? Nuxt does the same and performs well.

I am genuinely confused. Take a look at the source, let me know if I could be doing anything differently, but I don't mean special optimizations — the point was to test the vanilla setup of these frameworks.

Also, this is of course not a fair comparison. With @fastify/vue and @fastify/react, you get the absolute minimum necessary. They're as minimal as it can get. Nuxt and Next are behemoths that do and account for everything.

They're Swiss Army knives.

At some point things get big enough that they stop being tools, and become products. You're supposed to embrace the Next way or the Nuxt way, and that's fair, but it comes at a cost. Suddenly you're using something that is prepared for all edge cases, includes all batteries and then some, when in most cases you just need a tiny piece of it — the seamless transition from CSR to SSR — the magic that happens when you SSR some client code and when it loads on the browser, it starts working as an SPA. In the end, this simple thing is the main functionality of all frameworks. Coupled with a small number of other goodies, that's what @fastify/vue and @fastify/react provide.

Update: Theo breaks it down.