Performance

Memory and hosting guidance, payload limits and the design choices behind the adapter.

Benchmarks cover page rendering, partial reloads, deferred props, SSR, caching and state storage. These controlled microbenchmarks are not production throughput estimates.

Memory and hosting

The useful planning number is not a microbenchmark; it is how much memory the whole process needs at rest and under load. These are reference points measured on Linux x64 after the first request, not guarantees:

App shapeRSS at restPlan for
Inertia.Net JIT app, no database (samples/MinimalApi.React)about 90 MB256–512 MB
Inertia.Net Native AOT app (tests/Inertia.Net.AotSmoke)about 42 MB128–256 MB
Small JIT todo: Minimal API + EF Core + SQLite + cookie authroughly 120–180 MB256–512 MB
Small Native AOT todo: source-generated JSON + SQLiteroughly 45–70 MB128–256 MB
Add managed Bun SSR+30–60 MB for the Bun processadd 128 MB headroom

The two Inertia.Net baseline rows were measured from this repository on Linux x64; the todo rows are planning ranges, not benchmark claims. The JIT todo range includes the runtime, EF Core, SQLite, authentication and a small working set. PostgreSQL or MongoDB, image processing and a large cache move it upward; measure your own app under realistic concurrency. Native AOT removes most JIT and reflection overhead, but it does not make EF Core or a database free.

For containers:

  • Set a memory limit rather than relying on the host.
  • Leave headroom for the GC, the OS page cache and request bursts.
  • Expect SSR to add a second process. If the Bun renderer is optional, SsrMode.Preferred lets the app fall back to client rendering when it is unavailable.
  • Profile with real page sizes. A dashboard with deferred props and a paginated feed behaves very differently from a page that serializes every row.

The benchmark project still has the throughput and allocation probes:

Text
dotnet run -c Release --project tests/Inertia.Net.Benchmarks -- --filter '*'

Targeted probes: --render-probe, --ssr-probe, --mvc-probe, --payload-probe and --cache-probe.

Payload size

The page JSON is fully buffered before the first byte is sent, so a failing loader never produces a half-written 200. Each concurrent request therefore holds its whole page in memory (pooled, and sized from the component's recent pages).

Keep pages well under a megabyte:

Ssr.MaxPageBytes keeps larger pages off the SSR server and Ssr.MaxResponseBytes bounds its answer; neither limits the props.

Design notes

  • Single pass. One recursive walk filters props, runs loaders, writes JSON to a Utf8JsonWriter and collects the metadata. There is no intermediate object tree.
  • Pooled buffers. The page is written to a pooled IBufferWriter<byte>, and the path tracker is a pooled char[]. A path becomes a string only when metadata needs it.
  • Version check before the handler. A stale client costs one header comparison and a 409.
  • No per-request mutable singletons. Per-request state lives in an InertiaFeature on HttpContext.Features, so concurrent requests cannot leak history or flash settings into one another.
  • Plain DTOs take the fast path. A POCO, list or typed dictionary is walked only when its type can hold InertiaProp values (InertiaProp, object or dictionary members/elements; cached per type). Everything else is one Serialize call.
  • UTF-8 throughout. SSR output and the MVC tag helpers write UTF-8 bytes; nothing large is materialized as a .NET string.
  • Cached assets. The Vite manifest is parsed once, the root template is parsed once into pre-encoded UTF-8 segments, and the production tags are pre-rendered per entry set and path base. The version is a precomputed string compared ordinally. A missing manifest, template or SSR bundle is re-checked at most once per second.
  • Sync loaders stay sync. Func<T> loaders are not wrapped in closures.

Most per-request allocation in a real handler is the props you build, not the render.