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 shape | RSS at rest | Plan for |
|---|---|---|
Inertia.Net JIT app, no database (samples/MinimalApi.React) | about 90 MB | 256–512 MB |
Inertia.Net Native AOT app (tests/Inertia.Net.AotSmoke) | about 42 MB | 128–256 MB |
| Small JIT todo: Minimal API + EF Core + SQLite + cookie auth | roughly 120–180 MB | 256–512 MB |
| Small Native AOT todo: source-generated JSON + SQLite | roughly 45–70 MB | 128–256 MB |
| Add managed Bun SSR | +30–60 MB for the Bun process | add 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.Preferredlets 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:
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:
- paginate with
Scroll - defer slow data with
Defer - load on demand with
Optional - use a partial reload for refresh buttons and filters
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
Utf8JsonWriterand 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 pooledchar[]. 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
InertiaFeatureonHttpContext.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
InertiaPropvalues (InertiaProp,objector dictionary members/elements; cached per type). Everything else is oneSerializecall. - 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.