skip to content

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%

answer

  1. crawlers and first paint, not navigation
  2. $withoutSsr on HandleInertiaRequests
  3. Inertia::withoutSsr(['app/*'])
  4. disableSsr(bool or closure)
  5. Vite plugin ssr: false plus config

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().

solid answer

~40 s

SSR only improves the **first full page load**: crawlers and link previews get real HTML and users see content before JavaScript runs. It costs a second runtime to operate, an HTTP round trip per full load, and components that must render without `window`. Public marketing pages earn that; an authenticated dashboard usually does not, since search engines never see it. In Laravel you scope it with the `protected $withoutSsr = ['dashboard', 'admin/*'];` property on `HandleInertiaRequests`, or `Inertia::withoutSsr([...])` from a provider; patterns match like `$request->is()`. `Inertia::disableSsr()` takes a boolean or closure to switch the adapter off entirely, for example `Inertia::disableSsr(fn () => app()->runningUnitTests())`, and `config(['inertia.ssr.enabled' => false])` works per request. To drop SSR completely, disable both layers: `ssr: false` in the Vite plugin and `inertia.ssr.enabled` false.

code

php · 17 lines
php
<?php

namespace App\Http\Middleware;

use Inertia\Middleware;

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

    // Logged-in areas are never crawled: skip the SSR round trip there.
    protected $withoutSsr = [
        'app',
        'app/*',
        'admin/*',
    ];
}

go deeper

for a junior

Know that SSR helps crawlers and the first page load, and that it can be switched off for parts of an app.

for a middle

Name the controls: $withoutSsr, Inertia::withoutSsr(), Inertia::disableSsr() with a boolean or closure, the per-request config key, and the two-layer full opt-out.

for a senior

Argue where SSR earns its cost, scope it by route, keep tests independent of the SSR server, and make render failures visible.

for a principal

Set the product-level rule for which surfaces must be server-rendered, and revisit it as SEO, performance budgets and hosting costs change.

## What SSR actually buys an Inertia app In **Inertia**, server-side rendering happens only on a **full page load**: a first visit, a refresh, a new tab. Every later click is an Inertia visit that returns JSON and renders in the browser. So SSR improves: - **crawlability**: search engines and social link-preview bots receive the rendered page body and head tags; - **first paint**: users on slow devices see the pricing table before the JavaScript bundle has run; - **no-JavaScript resilience** for the first view. It does **not** speed up in-app navigation, and it does nothing for pages crawlers never reach. ## What it costs | Cost | Where it shows up | |---|---| | a second runtime | a Node or Bun process to build, monitor, restart on deploy and health-check | | latency | an HTTP call from PHP to the SSR server on every full page load | | code constraints | components must render without `window`, `document` or browser-only libraries | | silent degradation | failures fall back to client rendering, so breakage can go unnoticed | | payload | props appear in the rendered HTML and again in the page JSON used for hydration | The judgement is therefore per section of the product. The public site (home, features, pricing, blog) is where crawlers and first impressions matter. An authenticated dashboard, an admin panel or an internal tool gains little: no crawler logs in, and users there are already on a warm session. ## Scoping SSR to the marketing pages The Laravel adapter offers three levels of control. **1. Exclude paths.** Keep SSR on globally and list the paths that skip it: - the `$withoutSsr` property on your `HandleInertiaRequests` middleware, for example `['dashboard', 'dashboard/*', 'admin/*']`; - or `Inertia::withoutSsr(['dashboard*', 'admin/*'])`, typically in a service provider's `boot()`. Both feed the SSR gateway's exclusion list, matched with `$request->is()` or `fullUrlIs()`, so `*` wildcards work as in route patterns. **2. Disable the adapter conditionally.** `Inertia::disableSsr()` switches off dispatching entirely; it accepts a boolean or a closure resolved at render time: - `Inertia::disableSsr(app()->runningUnitTests())` keeps feature tests independent of a running Node server; - a closure can check a feature flag or an environment. Setting `INERTIA_SSR_ENABLED=false` in `phpunit.xml` achieves the same for tests. **3. Per request.** Inside middleware or a controller, `config(['inertia.ssr.enabled' => false])` turns SSR off for the current request only. ## Turning SSR off completely SSR has two layers, and a full opt-out disables both: 1. the **Vite plugin** (`@inertiajs/vite`) serves SSR in development and builds the SSR bundle: set `inertia({ ssr: false })`; 2. the **Laravel adapter** dispatches render requests: set `inertia.ssr.enabled` to `false` (env `INERTIA_SSR_ENABLED`). Turning off only the adapter still builds an unused bundle; turning off only the plugin leaves the adapter looking for a bundle it will never find. ## A worked setup A SaaS site serves `/`, `/features`, `/pricing` and `/blog/*` publicly, and `/app/*` after login: 1. keep `inertia.ssr.enabled` true and run the SSR server in production; 2. add `protected $withoutSsr = ['app', 'app/*'];` to `HandleInertiaRequests`; 3. call `Inertia::disableSsr(app()->runningUnitTests())` in a service provider, or set the env var in `phpunit.xml`; 4. listen for `SsrRenderFailed` so a broken marketing render is logged, not silently degraded. The logged-in area now avoids the SSR round trip, and its components may use browser-only libraries freely, while the pages that must be crawlable keep full server-rendered HTML. ## Common mistakes - enabling SSR for the whole app and then fighting `window is not defined` in admin widgets nobody crawls; - assuming SSR makes navigation faster, when only the entry request is rendered; - disabling only the adapter or only the plugin and leaving the other half half-configured.

  • Why disable SSR in feature tests rather than start the SSR server?
    Feature tests assert on the Inertia page object, not on rendered HTML, so the SSR call only adds a dependency and time. With SSR on and no server, renders quietly fall back anyway. Browser tests that care about SSR output are the place for `throw_on_error`.
  • What does a pattern like 'app/*' in $withoutSsr match?
    The gateway checks each pattern with the request's `is()` and `fullUrlIs()` methods, so `app/*` matches paths such as `app/projects/5` but not `app` itself; list both when the section root is also a page.

saying these in an interview costs you the question

  • SSR makes every Inertia link click faster
  • Excluding a path from SSR also excludes it from Inertia
  • Setting ssr: false in the Vite plugin alone stops Laravel dispatching renders
  • Authenticated dashboards benefit most from SSR for search ranking
  • disableSsr() only accepts a boolean fixed at boot