SSR is opt-in. AddSvelteNet() alone registers client rendering, generated types,
remote functions, and ASP.NET integration, but it does not register an
ISvelteSsrEngine. Production islands therefore mount with hydrate: false and
SvelteNet never reads or executes the server bundle.
Choose one renderer by chaining it from the builder:
// In-process JavaScript; no external runtime is required.
// Requires the SvelteNet.Jint package.
builder.Services
.AddSvelteNet()
.AddJintSSR();
// V8 through an installed Node.js CLI.
builder.Services
.AddSvelteNet()
.AddNodeSSR();
// JavaScriptCore through an installed Bun CLI.
builder.Services
.AddSvelteNet()
.AddBunJsSSR();Calling another renderer method on the same builder replaces the previous selection.
Per-island ComponentOptions.Ssr = false and the shared SvelteOptions.EnableSsr = false can still disable an already configured renderer. Setting Ssr = true cannot
enable SSR when no engine was registered.
SSR is always skipped in Development because Vite serves source modules directly. Build and run with a non-Development environment to inspect server-rendered output.
Jint ships as a separate package so that client-only applications, and applications rendering through Node.js or Bun, do not carry a JavaScript engine they never load:
dotnet add package SvelteNet.JintJint runs inside the ASP.NET process and needs no Node.js installation. Engines cache their imported module graph and are pooled across renders:
builder.Services.AddSvelteNet().AddJintSSR(options =>
{
options.Timeout = TimeSpan.FromSeconds(5);
options.MaxPooledEngines = Environment.ProcessorCount;
options.MaxConcurrentRenders = Environment.ProcessorCount;
});Awaited remote queries use ISvelteSsrFetchHandler to dispatch directly to the
generated C# remote descriptor. There is no loopback HTTP request. The bridge enforces
the same authorization the HTTP endpoint would — see Security.
Two consequences of running a synchronous engine in an asynchronous host:
- Renders occupy a worker thread. Jint cannot suspend a running script to await a
task, so the fetch bridge blocks for the duration of each awaited query.
RenderAsyncmoves that work off the request thread and caps it withMaxConcurrentRenders, so the number of parked threads stays bounded however much traffic arrives. Renders beyond the cap queue rather than starving the pool. - Pooled engines share module state. An engine keeps its module graph — and any
module-level state in the SSR bundle — alive across renders and across users. Do not
hold per-request state at module scope in a
.sveltefile. The Node.js and Bun renderers start a fresh process per render and do not behave this way.
The Node.js and Bun implementations are included in SvelteNet.AspNetCore; no extra
NuGet integration package is required. They launch the configured CLI for server
renders, import the built ESM component and sveltenet/server entry, and return the
same SsrResult contract as Jint.
builder.Services.AddSvelteNet().AddNodeSSR(options =>
{
options.ExecutablePath = "/opt/node/bin/node"; // default: "node"
options.Timeout = TimeSpan.FromSeconds(8);
options.BaseUrl = new Uri("http://127.0.0.1:5000"); // optional trusted override
options.ForwardHeaders.Remove("Authorization");
});
builder.Services.AddSvelteNet().AddBunJsSSR(options =>
{
options.ExecutablePath = "/opt/bun/bin/bun"; // default: "bun"
options.Timeout = TimeSpan.FromSeconds(8);
});node or bun must be installed on the production host and available on PATH
unless an absolute ExecutablePath is supplied. The engine verifies the CLI on its
first render — resolving a singleton from DI should not spawn a process — and throws an
actionable error if it cannot run it.
During an HTTP render, relative fetches are resolved against an address reported by
the running ASP.NET server, never the incoming Host header. Set BaseUrl to a
trusted absolute application origin when automatic server addresses are unsuitable
(for example, behind a proxy or with Unix sockets). SvelteNet forwards incoming
Authorization and Cookie headers to that trusted origin by default, so
authenticated remote queries retain the request identity; edit or clear
ForwardHeaders to change that policy. Unlike Jint's in-process bridge, these
backends make a real loopback HTTP request.
Implement ISvelteSsrEngine when JavaScript should run in a persistent sidecar,
worker pool, embedded runtime, remote service, or any other host:
public sealed class MyRenderer : ISvelteSsrEngine
{
public ValueTask<SsrResult> RenderAsync(
string componentModule,
string renderModule,
string? propsJson,
CancellationToken cancellationToken = default)
{
// Execute the modules and return their head/body output.
throw new NotImplementedException();
}
}
builder.Services
.AddSvelteNet()
.AddCustomRenderer<MyRenderer>();An existing instance or DI factory also works:
builder.Services.AddSvelteNet().AddCustomRenderer(rendererInstance);
builder.Services
.AddSvelteNet()
.AddCustomRenderer(services => new MyRenderer(
services.GetRequiredService<MyRuntime>()));All renderer registrations are singletons. A custom renderer must therefore be thread-safe or manage its own worker/engine pool.
Rendering is asynchronous because a server render can await remote queries, which reach
databases and HTTP services. Never block the calling thread waiting on that work: a
synchronous JavaScript runtime should move its render onto a bounded worker, the way the
Jint engine does. An in-process renderer that wants the no-round-trip query bridge adds
it with .AddInProcessSsrFetchBridge().
One vite build emits the public client assets to wwwroot/client and the fully
bundled ESM server output to .svelte-net/server. The latter contains no runtime
node_modules dependency and must remain private. Deploy it when an SSR renderer is
selected; a client-only application does not execute it.
SvelteOptions.ServerOutput and the Vite plugin's serverOutDir must match if either
default is changed. See Options.