Server-Side Rendering

Copy Markdown View Source

LiveSvelte supports server-side rendering (SSR) of Svelte components, which provides meaningful HTML on the first paint before the JavaScript bundle loads.

How SSR Works

When SSR is active, the initial (dead) render calls Node.js to execute Svelte's render() function server-side. The result is embedded directly in the HTML response:

  1. LiveSvelte calls SSR.render(component_name, props, slots)
  2. render() returns %{"head" => "<style>...</style>", "html" => "<div>...</div>", "css" => %{"code" => "..."}}
  3. The head and CSS styles are included in the page; html is placed inside the data-svelte-target div
  4. When JavaScript loads, the SvelteHook hydrates the existing DOM instead of mounting fresh

SSR Modes

LiveSvelte has two SSR modules for different environments:

NodeJS Mode (Production)

Uses elixir-nodejs to run a pool of Node.js workers that execute the SSR bundle.

# config/prod.exs
config :live_svelte,
  ssr_module: LiveSvelte.SSR.NodeJS,
  ssr: true

The SSR bundle is built by:

mix assets.build  # runs phoenix_vite.npm vite build (client + SSR)

This produces priv/svelte/server.js, which the NodeJS supervisor loads on application start.

ViteJS Mode (Development)

Forwards SSR requests to the Vite dev server over HTTP. This provides instant HMR without rebuilding the SSR bundle on every change.

# config/dev.exs
config :live_svelte,
  ssr_module: LiveSvelte.SSR.ViteJS,
  vite_host: "http://localhost:5173"

ViteJS Mode Requires Vite Dev Server

LiveSvelte.SSR.ViteJS only works when mix phx.server is running alongside the Vite dev server started by Phoenix's watchers. If the Vite server is not running, SSR will silently fall back to client-only rendering.

Configuration

Enable/disable SSR globally:

# config/config.exs
config :live_svelte, ssr: true   # enabled (default)
config :live_svelte, ssr: false  # disabled

Select SSR module:

config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS   # production (default)
config :live_svelte, ssr_module: LiveSvelte.SSR.ViteJS   # development

Per-Component SSR Opt-Out

Disable SSR for a specific component:

<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />

Components with ssr={false} render a loading slot or nothing on the first paint, then mount client-side normally.

HMR in Development

When running with LiveSvelte.SSR.ViteJS, changes to Svelte files trigger automatic hot module replacement. The SvelteHook re-mounts affected components without a full page reload.

With phoenix_vite (recommended): The layout uses PhoenixVite.Components.assets. The Igniter installer adds to your endpoint in config/dev.exs a :vite watcher and static_url: [host: "localhost", port: 5173], so mix phx.server starts the Vite dev server and asset URLs point at it — Svelte and CSS then hot-reload with no extra terminal. See Installation for the exact config; if you added phoenix_vite or LiveSvelte manually, add that endpoint config yourself.

Without phoenix_vite: Use LiveSvelte.Reload.vite_assets/1 in your layout to include Vite's HMR client and production fallback:

<LiveSvelte.Reload.vite_assets assets={["/js/app.js"]}>
  <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
  <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</LiveSvelte.Reload.vite_assets>

Loading Slot

Show content while a component is loading (only when ssr={false}):

<.svelte name="SlowChart" props={%{data: @data}} socket={@socket} ssr={false}>
  <:loading>
    <div class="spinner">Loading chart...</div>
  </:loading>
</.svelte>

Loading Slot + SSR Incompatible

The :loading slot is mutually exclusive with SSR. Using both together produces a compile warning. If SSR is active, the loading slot is ignored.

Telemetry

LiveSvelte emits telemetry events for SSR operations:

EventWhen
[:live_svelte, :ssr, :start]SSR render begins
[:live_svelte, :ssr, :stop]SSR render completes; includes duration_microseconds measurement
[:live_svelte, :ssr, :exception]SSR render throws an exception

Attach handlers for observability:

:telemetry.attach(
  "live-svelte-ssr",
  [:live_svelte, :ssr, :stop],
  fn _event, measurements, _metadata, _config ->
    Logger.debug("SSR render took #{measurements.duration_microseconds}µs")
  end,
  nil
)

Testing with SSR

SSR is disabled in the test environment by default. To write tests that verify SSR output, enable it per test suite:

defmodule MyAppWeb.SsrTest do
  use MyAppWeb.ConnCase, async: false  # must be async: false

  setup do
    Application.put_env(:live_svelte, :ssr, true)
    on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
    :ok
  end

  test "renders SSR HTML", %{conn: conn} do
    html = conn |> get("/counter") |> html_response(200)
    assert html =~ "data-ssr=\"true\""
  end
end

Use get/2 + html_response/2 for initial HTML checks — visit/2 from PhoenixTest connects the socket and transitions past the dead render.

Production Deployment

See Deployment for complete Node.js setup instructions for production.