Laravel Daily's

SSR with Laravel, Vue, and Inertia: Faster First Paint, Same Elegant DX

hero image

Server-side rendering gives your Laravel and Vue application a head start.

Instead of sending an almost empty HTML shell, Laravel can return a page that already contains rendered Vue markup. The browser displays useful content sooner. Vue then hydrates that markup and makes it interactive.

Inertia keeps the application model familiar. Your routes, controllers, validation, and page components stay in place. SSR adds another rendering path for the initial request.

This makes SSR a practical option for teams building modern applications with a PHP web framework.

What Inertia SSR does

A standard Inertia request follows a simple path:

  1. The browser requests a Laravel route.
  2. Laravel runs the controller.
  3. Inertia returns a page object with a component name and props.
  4. The browser loads JavaScript.
  5. Vue renders the page inside the application shell.

With SSR enabled, the first request takes a different path:

  1. The browser requests a Laravel route.
  2. Laravel runs the controller and creates the Inertia page object.
  3. Laravel sends that page object to the Inertia SSR server.
  4. A Node process renders the Vue component tree into HTML.
  5. Laravel sends the rendered HTML to the browser.
  6. Vue hydrates the existing markup on the client.

The Node process uses Inertia’s server rendering layer. In setups that expose the standalone package, this is commonly described as @inertiajs/server. In the current Vue 3 adapter examples, createServer comes from @inertiajs/vue3/server.

The result is still an Inertia application. SSR only changes how the initial HTML is produced.

Pipeline showing Laravel sending an Inertia page to a Node SSR renderer before the browser hydrates the Vue interface

Why use SSR with Laravel and Vue?

SSR primarily improves the first visit.

A client-rendered page may initially contain only a root element. The browser must download JavaScript, evaluate it, resolve the page component, and render the interface. Users may see a blank state during that work.

SSR sends meaningful HTML immediately. The browser can start displaying headings, navigation, product details, or article content before the client bundle finishes loading.

This often improves perceived performance. The application feels ready earlier, even when the total JavaScript work remains similar.

Faster first paint

SSR can reduce the time between a request and visible content. This matters most for public pages, landing pages, product catalogs, documentation, and content-heavy dashboards.

The improvement depends on your application and infrastructure. SSR does not make every request faster. It moves some rendering work from the browser to the server.

Better indexing for dynamic pages

Search engines can process JavaScript. They still benefit from receiving complete HTML.

SSR makes dynamic content available in the initial response. That gives crawlers a clearer document to inspect. It also supports server-rendered page titles and metadata through Inertia’s <Head> component.

SSR is not a replacement for good SEO. You still need meaningful titles, canonical URLs, structured content, and accessible markup. It simply removes one rendering dependency from the indexing path.

A smoother experience on slower devices

Desktop development machines hide a lot of client-side work. Lower-powered phones and slower networks expose it.

SSR lets those devices display the initial page before completing all client-side work. After hydration, navigation continues through Inertia without full-page reloads.

That combination is the main appeal. You get server-rendered entry points and SPA-style navigation afterward.

How hydration fits into the model

Hydration is different from a normal Vue mount.

A normal mount creates the interface from scratch. Hydration starts with HTML that Vue already expects to exist. Vue connects its component tree, state, and event listeners to that markup.

Your client entry must use createSSRApp:

import { createInertiaApp } from '@inertiajs/vue3'
import { createSSRApp, h } from 'vue'

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob('./Pages/**/*.vue')

        return pages[`./Pages/${name}.vue`]()
    },

    setup({ el, App, props, plugin }) {
        createSSRApp({
            render: () => h(App, props),
        })
            .use(plugin)
            .mount(el)
    },
})

The server and client must produce compatible markup. A timestamp generated independently on both sides can cause a mismatch. So can random values, different locale formatting, or browser-only APIs such as window and document.

Move browser-specific work into lifecycle hooks such as onMounted. Keep the initial render deterministic.

Vue hydration illustration showing server-rendered HTML becoming interactive through Vue and Inertia client-side behavior

Enabling SSR in an Inertia 3.x application

Laravel starter kits support Inertia SSR. If you use a current Vue starter kit, check its existing scripts before adding custom wiring.

The Laravel starter kit documentation covers the supported SSR workflow. The current Inertia documentation also provides the SSR setup guide.

For a custom setup, install the Vite plugin and Vue’s server renderer:

npm install @inertiajs/vite @vue/server-renderer

Add the Inertia plugin to vite.config.js:

import inertia from '@inertiajs/vite'
import laravel from 'laravel-vite-plugin'
import { defineConfig } from 'vite'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.js'],
            refresh: true,
        }),

        inertia({
            ssr: {
                entry: 'resources/js/ssr.js',
                host: '127.0.0.1',
                port: 13714,
            },
        }),
    ],
})

The Inertia Vite plugin can detect and manage the SSR entry automatically. Explicit configuration is useful when you need a separate entry point or a custom server port.

Update your build script so it produces both bundles:

{
    "scripts": {
        "dev": "vite",
        "build": "vite build && vite build --ssr"
    }
}

Create resources/js/ssr.js:

import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import { createSSRApp, h } from 'vue'
import { renderToString } from 'vue/server-renderer'

createServer((page) =>
    createInertiaApp({
        page,

        render: renderToString,

        resolve: (name) => {
            const pages = import.meta.glob('./Pages/**/*.vue')

            return pages[`./Pages/${name}.vue`]()
        },

        setup({ App, props, plugin }) {
            return createSSRApp({
                render: () => h(App, props),
            }).use(plugin)
        },
    }),
)

The SSR entry should include the same plugins and global configuration that your client entry needs. Shared layouts, translations, and UI plugins must work in both environments.

Connect Laravel to the SSR server

The Inertia middleware remains part of your normal Laravel web middleware stack. It shares props, resolves the root view, and participates in the request lifecycle as usual.

A typical middleware still looks like this:

<?php

namespace App\Http\Middleware;

use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    protected $rootView = 'app';

    public function share($request): array
    {
        return array_merge(parent::share($request), [
            'appName' => config('app.name'),
        ]);
    }
}

The Laravel adapter uses its SSR configuration to contact the Node process:

// config/inertia.php

'ssr' => [
    'enabled' => env('INERTIA_SSR_ENABLED', true),
    'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'),
    'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
],

Keep the SSR server bound to a private interface when Laravel and Node run on the same machine. The SSR endpoint does not need to be publicly accessible.

During development, the Inertia Vite plugin can handle SSR through the Vite server. Run:

npm run dev

For production, build the application and start the SSR process:

npm run build
php artisan inertia:start-ssr

The Laravel Vite documentation provides additional guidance for asset builds. The Inertia command starts the Node runtime with the generated SSR bundle.

A quick real-world walkthrough

Consider a public product page at /products/laravel-cloud.

Your Laravel controller can remain conventional:

use App\Models\Product;
use Inertia\Inertia;

public function show(Product $product)
{
    return Inertia::render('Products/Show', [
        'product' => $product,
        'related' => $product->relatedProducts()->limit(4)->get(),
    ]);
}

The Vue page receives the same props as it would without SSR:

<script setup>
import { Head } from '@inertiajs/vue3'

defineProps({
    product: Object,
    related: Array,
})
</script>

<template>
    <Head :title="product.name" />

    <main>
        <h1>{{ product.name }}</h1>
        <p>{{ product.description }}</p>

        <a :href="`/products/${product.slug}/checkout`">
            Choose this product
        </a>
    </main>
</template>

When a visitor opens the URL, Laravel builds the page object. The SSR server renders the heading, description, and related products into HTML.

The visitor sees the product information before Vue finishes loading. Once the client bundle arrives, Vue hydrates the existing document. The checkout link and other interactive controls then behave normally.

When the visitor opens another Inertia link, the application uses client-side navigation. The server does not need to render every subsequent page into the first response.

Production illustration of a Laravel application and Node SSR process running together with monitoring and health checks

The trade-offs

SSR adds infrastructure.

Your production environment now needs a compatible Node runtime. Inertia 3.x requires Node.js 22 or higher for its SSR server. You also need a process monitor, deployment restart strategy, and enough memory for the additional process.

SSR adds server work, too. Every SSR-enabled initial request needs Vue rendering on the server. High-traffic applications may need caching, horizontal scaling, or SSR clustering.

The browser still downloads and runs JavaScript. SSR does not remove the client bundle. It changes when users receive visible markup.

SSR also increases the number of failure modes. A component that references window during setup may work in the browser but fail on the server. A server and client rendering different values can produce hydration warnings.

Inertia falls back to client-side rendering when SSR fails by default. That protects the request, but it can hide broken SSR paths. You can listen for SsrRenderFailed and enable throw_on_error in tests to catch these issues early.

When SSR is not worth it

SSR may be unnecessary for private applications behind authentication. A project management dashboard, internal admin panel, or operational tool may receive little SEO value from server-rendered HTML.

It may also be a poor fit for pages with highly browser-dependent interfaces. WebGL canvases, live device APIs, and deeply interactive editors often need careful SSR boundaries.

Start with the routes that benefit most. Public pages usually provide the clearest return. You can exclude route groups through the Inertia middleware when needed:

protected $withoutSsr = [
    'admin/*',
    'dashboard',
];

This gives you a mixed strategy. Marketing and content pages receive SSR. Private application screens keep the simpler client-rendered path.

A useful addition to the Laravel toolkit

SSR is one more focused tool in the Laravel ecosystem.

It does not ask you to replace your controllers with a separate API layer. You do not need to build a REST API with PHP just to deliver the first page. You keep Laravel routing and server-side data access, while Vue handles the interface.

That is the broader value of modern PHP developer tools. Each part solves a clear problem without forcing the rest of your application into a new shape.

Laravel handles the request. Inertia carries the page data. Node renders the first view. Vue hydrates it and manages the interface afterward.

For public, dynamic pages, that division can deliver faster first paint with the same elegant development experience.

Previous
AI Email Automation in Laravel: Drafting, Classifying, and Replying at Scale
Next
Optimistic UI in Inertia 3.x: Instant Feedback Without the Jank