Inside Inertia SSR: How the Server and Client Work Together

So you’re working with Inertia.js and thinking about adding SSR? You’re in good company. Server-side rendering can seriously improve performance and SEO, and with Vue 3 in the mix, it’s surprisingly powerful. But it can also be a bit of a black box if you’re not sure what’s going on behind the scenes.

In this article, I’ll walk you through exactly what happens when you start the Inertia SSR server and build your client bundle - what gets loaded, what gets rendered, and how everything syncs up between client and server.

If you’re trying to wrap your head around how Inertia handles SSR or just want to make sure you’re not leaking state between users, this one’s for you.

What is SSR (and Why Should You Care)?

SSR stands for Server-Side Rendering, and at its core, it means rendering your frontend views (like your Vue components) into HTML on the server, before sending them to the browser.

Normally, with a single-page app (SPA), the server just sends a blank HTML shell and a big chunk of JavaScript. The browser then downloads that JavaScript, runs it, and finally renders the actual UI - this is called client-side rendering. It works, but it means users have to wait a bit before they see anything useful on the screen.

SSR flips that around: the server does the heavy lifting first. It generates a full HTML page based on your Vue components and sends that straight to the browser. The browser can show content immediately, and then Vue “hydrates” (what is hydration) the page - making it interactive - once the JavaScript finishes loading.

When to use SSR

Here’s what you get when you use SSR with Inertia and Vue:

SSR also adds complexity. You have to think about state management, avoid global variables, and make sure your app behaves the same on both server and client. But when it’s set up right - like with Inertia - the benefits can be well worth it.

Laravel vs Inertia Daemon: What Actually Handles the Request?

Now that we understand the difference between SPA and SSR, let’s talk about the two different places where a request can be handled in an SSR-enabled Inertia app:

At a glance, it might seem like they behave the same - but under the hood, they’re quite different. Especially when it comes to global state.

Solely Laravel Requests

Every time a request hits Laravel, it:

This means that global variables don’t persist between requests.

$GLOBALS['randomNumber'] = rand(0, 5);

That variable is gone as soon as the request ends. There’s no risk of cross-request data leakage, even if 100 users are hitting your app at the same time. Every user will receive a new random number on every request.

Inertia SSR Daemon

The Inertia SSR daemon is a long-running Node.js server that your Laravel app talks to when it needs HTML rendered for a Vue component.

This long running Node.js server is available once you run:

php artisan inertia:start-ssr

Because it stays alive across multiple requests:

For example:

// server.js (Inertia SSR daemon)
let randomNumber = Math.random()

app.get('/', (req, res) => {
  return { randomNumber }
})

Once we start the server, randomNumber gets evaluated and it is kept in memory, which means that every user will receive the same number until we restart the server.

This is important to know because you can easily introduce bugs in your VueJS/React application without knowing it. Let’s say you’ve got the following setup to handle real-time events:

// services/websockets.js
import Echo from 'laravel-echo';

export const broadcaster = new Echo({
  broadcaster: 'pusher',
  client: window.pusherInstance,
})

// App.vue
<script setup>
import {broadcaster} from '@/services/websockets'
import {onMounted} from 'vue'

onMounted(() => {
  broadcaster.on('email:verified', () => {
    // Handle real-time event
  })
})
</script>

In an SSR setup, this code is quietly causing a memory leak and cross-request side effects.

That means:

⚠️ Memory grows. Listeners stack up. Things get weird.

Fixing this issue is quite easy, we should move the creation of the Echo instance inside a function, so that each request or component creates its own instance.

// services/websockets.js
import Echo from 'laravel-echo'

export function createBroadcaster(pusherClient) {
  return new Echo({
    broadcaster: 'pusher',
    client: pusherClient,
  })
}