skip to content

SSR & Stale-Asset Reloads

An Inertia app can prerender pages in a Node or Bun SSR server and forces a full reload when the asset version changes. Interviewers ask when SSR earns its cost and how stale tabs recover.

on this pageshow

explore

questions

5

In an Inertia 3 Laravel app, how does server-side rendering produce the HTML for a public pricing page, and which requests does it apply to?

level: middleimportance: must knowfreq 38%

answer

  1. only the first, full page load
  2. the root view's directives call the gateway
  3. POST the page object to /render
  4. port 13714, head and body back
  5. failure falls back to client rendering

basics

~20 s

On a full page load, the root Blade view asks Inertia's SSR gateway to POST the page object to a Node or Bun server, which returns head and body HTML; later Inertia visits get JSON only, and any SSR failure falls back to client rendering.

solid answer

~40 s

SSR only touches **full page loads**: a request without `X-Inertia` renders the root Blade view, and its `@inertia` / `@inertiaHead` directives (or `<x-inertia::app>` / `<x-inertia::head>`) call the SSR gateway once per request. The gateway POSTs the page object (component, props, URL, version) to the SSR server's `/render` endpoint, `http://127.0.0.1:13714` by default (`inertia.ssr.url`), or to Vite's `/__inertia_ssr` endpoint while `npm run dev` is running hot. The server renders the page component and returns `head` and `body`, which the directives echo into the HTML, and the client adapter then hydrates it. Every later Inertia visit returns JSON and is rendered in the browser. If SSR is disabled, the bundle is missing or the request fails, the view falls back to the page JSON plus an empty root element, and an `SsrRenderFailed` event fires.

code

html · 13 lines
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    @vite('resources/js/app.js')
    <x-inertia::head>
        <title>{{ config('app.name') }}</title>
    </x-inertia::head>
</head>
<body>
    <x-inertia::app />
</body>
</html>

go deeper

for a junior

Remember that SSR sends real HTML for the first page load so crawlers see content, and that normal Inertia navigation stays client-side.

for a middle

Walk the flow: root view directives, the gateway POST to /render on port 13714, head and body echoed, hydration, and the client-rendering fallback.

for a senior

Reason about failure modes and cost: silent fallbacks, timeouts on the SSR call, browser-only code in components, and the extra process to operate.

for a principal

Decide whether a product needs SSR at all or only on some sections, weighing crawlability and first paint against operating a second runtime.

## Why an Inertia app needs SSR at all Without server-side rendering, an **Inertia** page's first response is a Blade shell: a root `<div>` and the page object as JSON. The browser downloads the JavaScript bundle, resolves the page component (for example `Marketing/Pricing`) and renders it. A crawler that does not execute JavaScript, or a link-preview bot, sees an empty body. For public marketing pages that must be crawlable, **SSR** renders the same component on a JavaScript server so the first response already contains the pricing table. ## The first request, step by step 1. The browser requests `/pricing`. The request has no `X-Inertia` header, so `Inertia::render('Marketing/Pricing', $props)` returns the **root view** (`app.blade.php` by default) with the page object. 2. The root view contains `@inertiaHead` in `<head>` and `@inertia` in `<body>`, or the Blade components `<x-inertia::head>` and `<x-inertia::app>`. 3. The first of these asks the **SSR gateway** to dispatch. An internal `SsrState` object makes sure the gateway is called **once** per request and shares the result between head and body. 4. The default `HttpGateway` POSTs the page object with Laravel's HTTP client: - in production, to `inertia.ssr.url` plus `/render` (default `http://127.0.0.1:13714/render`); - while Vite runs hot in development, to the Vite dev server's `/__inertia_ssr` endpoint, so no separate SSR process is needed. 5. The SSR server renders the component and answers with JSON holding `head` (an array of tags) and `body` (the rendered root element). 6. `@inertiaHead` echoes the head tags and `@inertia` echoes the body. 7. In the browser, the client adapter **hydrates** the existing markup instead of rendering from scratch. ## Which requests are server-rendered | Request | SSR involved? | |---|---| | first load, refresh, or link opened in a new tab | yes | | a `<Link>` click or `router.visit` (an Inertia visit) | no, JSON page object only | | a form submission and its redirect | no | | a forced full reload after an asset version change | yes, it is a full page load | This is why SSR costs little per navigation: only the entry request pays for it. It is also why SSR mainly helps crawlers and first paint, not in-app navigation speed. ## When SSR is skipped The published `config/inertia.php` sets `ssr.enabled` to true, yet SSR only happens once a bundle and a server exist. The gateway returns nothing, and the view falls back to client-side rendering, when: - `inertia.ssr.enabled` is false, `Inertia::disableSsr()` applies, or the path matches `withoutSsr`; - not running hot, and no SSR bundle is found (for example `bootstrap/ssr/ssr.js` or `bootstrap/ssr/app.js`), while `ensure_bundle_exists` is true; - the HTTP call fails, times out (`inertia.ssr.timeout`) or returns an error. In the fallback, `@inertia` prints `<script data-page="app" type="application/json">` with the page object and an empty `<div id="app">`, which is exactly the non-SSR output. On failures the adapter dispatches an **`SsrRenderFailed`** event you can listen for, and `inertia.ssr.throw_on_error` turns the silent fallback into an exception for test runs. ## What changed in Inertia 3 - **SSR in development** works through the `@inertiajs/vite` plugin: the Laravel adapter detects Vite's hot file and renders through the dev server. - The initial page is always delivered in a `<script type="application/json">` element, not a `data-page` attribute on the root `<div>`. - Blade components `<x-inertia::head>` and `<x-inertia::app>` join the directives; the head component's slot is a fallback printed only when SSR did not render. ## Costs to keep in mind - a long-running Node (or Bun) process to deploy, monitor and restart; - an extra local HTTP round trip on every full page load; - page components must not touch `window` or `document` during render; - props are serialised twice: into the rendered HTML and into the page JSON for hydration.

  • Does clicking a <Link> to /features trigger another SSR render?
    No. The click is an Inertia visit with `X-Inertia`, so Laravel answers with the page object as JSON and the browser renders the new component. Only full page loads go through the root view and its SSR directives.
  • How would you notice that production SSR has silently stopped working?
    Pages still work because the view falls back to client rendering. Listen for `Inertia\Ssr\SsrRenderFailed` and log it, run `php artisan inertia:check-ssr` as a health check, and in tests set `INERTIA_SSR_THROW_ON_ERROR=true` so a failed render throws instead of falling back.

saying these in an interview costs you the question

  • Every Inertia visit is rendered on the Node server
  • PHP renders the React or Vue components itself
  • An SSR failure returns a 500 error page to the visitor
  • Development needs inertia:start-ssr running beside npm run dev
  • The head and the body each trigger their own SSR request
open as a page

With Inertia's <Head> component, how do you set a marketing page's title and meta description without producing duplicate head tags?

level: juniorimportance: should knowfreq 32%

basics

~20 s

Render <Head> from @inertiajs/react or @inertiajs/vue3 with a title prop or child tags; Inertia keeps a single <title>, stacks other tags, and a head-key lets a page replace a layout's meta tag instead of duplicating it.

open as a page

In an Inertia Laravel app, how does the middleware's version() method make a stale browser tab reload after you deploy new frontend assets?

level: seniorimportance: should knowfreq 40%

basics

~20 s

version() returns a hash of Vite's build manifest; each Inertia visit sends the version it loaded with, and when a GET visit's version differs the server answers 409 with X-Inertia-Location, so the client does a full page load and fetches the new assets.

open as a page

When is Inertia SSR worth its cost, and how would you limit it to crawlable marketing pages using withoutSsr or disableSsr in Laravel?

level: seniorimportance: should knowfreq 30%

basics

~20 s

SSR pays off for public, crawlable pages and first paint, not for logged-in screens; keep it on for marketing routes and exclude the rest with the middleware's $withoutSsr or Inertia::withoutSsr(), and switch it off in tests with Inertia::disableSsr().

open as a page

In production, how do you build, start, health-check and restart Inertia's SSR server with Artisan, and what does --runtime change?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Build the client and SSR bundles, run php artisan inertia:start-ssr under a process monitor, verify it with inertia:check-ssr, and on each deploy run inertia:stop-ssr so the monitor restarts it on the new bundle; --runtime picks node, bun or a binary path.

open as a page