Server-Side Rendering (SSR) is an often overlooked aspect when building high-performance web applications with Node.js.
During my time consulting, many engagements centered around debugging Node.js performance issues. In these scenarios, the culprit is almost always SSR. SSR is a CPU bound activity that can easily be the main cause of blocking Node.js' event loop. It's crucial to consider this when choosing your frontend stack.
We set out to find out the state of SSR performance across today's most popular libraries, especially those that can be cleanly integrated with Fastify.
For this, we needed to generate a non-trivial sample document that includes a large number of elements, to have a very large page for the test, and consequently have more running time to capture each libraries' performance.
So, we asked an LLM to write some code to draw a spiral into a container using divs as 10x10px tiles:
<script>
const wrapper = document.getElementById('wrapper')
const width = 960
const height = 720
const cellSize = 5
function drawSpiral() {
let centerX = width / 2
let centerY = height / 2
let angle = 0
let radius = 0
const step = cellSize
while (radius < Math.min(width, height) / 2) {
let x = centerX + Math.cos(angle) * radius
let y = centerY + Math.sin(angle) * radius
if (x >= 0 && x <= width - cellSize && y >= 0 && y <= height - cellSize)
{
const tile = document.createElement('div')
tile.className = 'tile'
tile.style.left = `${x}px`
tile.style.top = `${y}px`
wrapper.appendChild(tile)
}
angle += 0.2
radius += step * 0.015
}
}
drawSpiral()
</script>
Subsequently, we asked it to create versions of it using all libraries we intended to test, with the implementation adapted to use each libraries' rendering engine rather than relying on the DOM methods of the original example.
This is what our sample document looks like with all its 2398 <div>
elements:
Fastify's Vite integration setup makes for the perfect testbed for investigating where SSR performance is at for various frameworks.
In this article, we'll look at the minimum required boilerplate for performing SSR, and compare the performance of five major frontend libraries: React, Vue, Solid, Svelte and Preact. We also looked at fastify-html (a Fastify wrapper for ghtml) and ejs via @fastify/view for simpler alternatives.
We chose not to consider tools like Next.js, Astro and Qwik, as well as other full fledged frameworks, as they do not offer isolated rendering methods.
For @fastify/vite-based tests, we used a boilerplate as follows:
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'
const server = Fastify()
await server.register(FastifyVite, /* options */)
await server.vite.ready()
await server.listen({ port: 3000 })
All tests were run against the production build, that is, after running vite build
.
The only exceptions are the fastify-html and ejs tests, which don't require Vite.
Check out the repository containing all examples.
Ensuring consistency
We've made sure all examples share the same characteristics:
No client-side reactivity features used.
All style bindings are done using template literals unless where inappropriate for the framework in question, as is the case for React and Solid.
The x and y values are created with toFixed(2).
No <style> tags other than the one in the document shell.
Tests were run on a 2020 MacBook Air M1 with 8GB RAM, Node v22 on macOS Ventura.
fastify-html
We begin with the outlier: fastify-html, a Fastify plugin wrapping ghtml, delivering 1088 requests per second. As mentioned earlier, this setup differs from all the others in that it doesn't require Vite, since no special syntax or transformations are required.
fastify-html was added to this test as a baseline. It doesn't really compare well with all others because it's just a wrapper for a simple HTML templating library, with none of the advanced features of the other libraries. Due to its simpler nature we already expected it to be the better performing library, and we wanted to see just how far behind the other fully featured libraries would be.
The boilerplate used can be seen below — notice that createHtmlFunction
(mimicking @fastify/vite) is used to register a layout function that renders the document shell:
import Fastify from 'fastify'
import fastifyHtml from 'fastify-html'
import { createHtmlFunction } from './client/index.js'
const server = Fastify()
await server.register(fastifyHtml)
server.addLayout(createHtmlFunction(server))
For reference, we've also added a test using the old school EJS (based on @fastify/view). It was able to handle 443 requests per second.
Vue
In second place, delivering 1028 requests per second, Vue is probably the best deal if you want great SSR performance and want a truly comprehensive library ecosystem.
The Vue API for synchronous server-side rendering used is renderToString()
:
import { renderToString } from 'vue/server-renderer'
// ...
await server.register(FastifyVite, {
async createRenderFunction ({ createApp }) {
return async () => ({
element: await renderToString(createApp())
})
}
})
Svelte
In third place, Svelte 5 (still a pre-release) delivered a whopping 968 requests per second, which is rather impressive considering its rich feature set.
Svelte has its own non-JSX templating syntax and its engine appears to be remarkably efficient, making it an excellent choice if you need a framework with a mature library ecosystem and don't want to compromise on SSR performance.
The Svelte API for server-side rendering used is render()
, from Svelte 5.
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction ({ Page }) {
return () => {
const { body: element } = render(Page)
return { element }
}
}
})
Note that the render()
function will also return head and body properties.
Solid
In fourth place comes SolidJS, delivering 907 requests per second. It falls behind Svelte by a very low margin. Solid is a very promising alternative to React, but still has a maturing ecosystem.
One thing we noticed is that SolidJS actually suffers from using IDs as part of its hydration process. Compare the markup generated by Vue and Solid, respectively:
<div class="tile" style="left: 196.42px; top: 581.77px">
<div data-hk=1c2397 class="tile" style="left: 196.42px; top: 581.77px">
This means a lot of the performance toll is coming from this extra fragment that needs to be sent over the wire. Still, we wanted to verify exactly that: how frameworks would behave under normal, real-world circumstances which meant having client facilities such as hydration enabled.
For the boilerplate, we used @fastify/vite's createRenderFunction
hook to capture the Solid component function (createApp
):
import { renderToString } from 'solid-js/web'
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction ({ createApp }) {
return () => {
return {
element: await renderToString(createApp)
}
}
}
})
Preact
React's popular younger brother comes in fifth place, delivering 717 requests per second. Even though Preact is very similar to React, there are many differences that make it faster and more lightweight.
The Preact API used for synchronous server-side rendering is renderToString()
:
import { renderToString } from 'preact-render-to-string'
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction ({ createApp }) {
return () => {
return {
element: renderToString(createApp())
}
}
}
})
React
React 19 RC comes in sixth place, delivering 572 requests per second.
The React API used for synchronous server-side rendering is renderToString()
:
import { renderToString } from 'react-dom/server'
// ...
await server.register(FastifyVite, {
root: import.meta.url,
createRenderFunction ({ createApp }) {
return () => {
return {
element: renderToString(createApp())
}
}
}
})
Wrapping up
💡 So what do these results mean?
At the top we see fastify-html and Vue, followed by Svelte and Solid close behind. Vue and Svelte probably offer the best trade-off between SSR performance and ecosystem maturity.
As mentioned before, the fastify-html test was added as a baseline, and to demonstrate what can be gained in performance by doing away with full fledged frontend frameworks and sticking to minimal templating.