Every page interactive on the server because it was convenient, and later a thousand open connections sit in memory. Or every page in WebAssembly, and the first visit downloads megabytes that a contact list never needed. In this lesson, the choice of render mode becomes a decision with costs that you justify and write down, instead of a default that you inherit.
Where the Kladde stands
The lesson assumes K2, above all 2.5.1 (the modes as an introduction), 2.5.8 (the first server island) and 2.5.10 (the WebAssembly island, InteractiveAuto in brief, the first entry in docs/decisions.md). The Kladde consists of src/Kladde.Web (Blazor Web App, .NET 10) and src/Kladde.Web.Client; the public pages are static, the internal area under /internal has islands. Keep your own names and carry the steps over.
One decision of this course deliberately departs from K2: K2 2.5.10 ruled out WebAssembly for the Kladde. From here on, the contact list under /internal/contacts gets a WebAssembly island, because W8.2.1 and W8.3.1 both need both paths: the one over the connection and the one over the API. For five people in the office the server island would be cheaper here too, and the decision folder says so explicitly.
Four modes, three costs
What the modes are is in K2 2.5.1; here what counts is what each one costs. There are three items, and each hits a different mode.
The round trip. In a server island, every event travels over the network to the server, the component renders there again, and the changes to the DOM travel back. The response time of the UI is the latency of the connection plus the computing time. Learn recommends a sustained UI latency of at most 250 ms. With a WebAssembly island, a click costs no round trip, but every access to data is a request to the API.
The connection. A server island keeps a circuit, including its state, in memory for every open browser tab. Learn names about 250 KB for a minimal Hello World app and budgets at least 1.3 GB for 5,000 concurrent users, about 273 KB per person; the real size depends on the state of your components and has to be measured. According to Learn, with a single server, memory usually runs out first. A circuit comes into being with the first server component on a page and ends when none is left on it. If the connection drops, the server keeps the circuit ready for a while: according to CircuitOptions, by default at most 100 disconnected circuits and 3 minutes.
The download. A WebAssembly island downloads the runtime and the bundle on the first visit and stores them in the cache. K2 2.5.10 measured about 2.3 MB on the Kladde's spike; your number depends on your code and gets reduced in W8.3.1. In return, the server only answers API requests.
| Question | Static | Interactive Server | Interactive WebAssembly |
|---|---|---|---|
| Round trip per event | none (new page) | yes, network plus computing time | no; data over the API |
| Cost on the server | one request | memory and one connection per tab | only the requests to the API |
| Cost at load | only HTML | blazor.web.js and the connection |
plus runtime and bundle |
| Access to data | directly from the service | directly from the service | only over an API |
| What the browser sees | HTML | HTML | HTML, the C# code and the prerendered state |
InteractiveAuto is not a fourth column but the choice between the two on the right: first server, then WebAssembly, with the costs of both.
Where interactivity is set
The default is static. You set the mode in three places.
On the component where it is used (@rendermode as an attribute): the parent decides, and the same component can stay static elsewhere.
<Dialog @rendermode="InteractiveServer" />
On the definition (@rendermode as a directive): it applies to every use and is the usual way for pages. The directive requires a static instance.
@page "/internal/calendar"
@rendermode InteractiveServer
For the whole app: on the topmost interactive piece that is not a root component, in the Routes template in App.razor. The Router passes the mode on to the pages, and HeadOutlet gets the same one. The root App itself cannot be interactive.
<HeadOutlet @rendermode="InteractiveServer" />
<Routes @rendermode="InteractiveServer" />
dotnet new blazor with the option -ai (--all-interactive) creates an app like this. If you choose WebAssembly or Auto for the whole app, the layout, pages and Routes have to move into the .Client project. For an app with many static pages like the Kladde, it therefore stays with islands: one line per island, everything else untouched.
The registration in Program.cs for both worlds:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Kladde.Web.Client.RenderInfo).Assembly);
MapStaticAssets() has to come before AddInteractiveWebAssemblyRenderMode(); without it, this lesson's throwaway project wrote the warning "Mapped static asset endpoints not found" at startup. AddAdditionalAssemblies tells the server about the .Client project.
The rules of passing a mode down
- Without an entry, a component inherits the mode of its parent; without any entry at all, it is static.
- Siblings may have different interactive modes.
- A child cannot switch to a different interactive mode than its parent. A server component under a WebAssembly component ends at run time with "Cannot create a component of type … because its render mode … is not supported by Interactive Server rendering".
- Parameters from a static parent component to an interactive child component have to be serializable to JSON. A
RenderFragmentsuch asChildContentdoes not work ("… the parameter is of the delegate type … which is arbitrary code and cannot be serialized").
Against the fourth rule, a wrapper component without the parameter helps, and the parent gives the mode to the wrapper; that is how the template does it with Routes around the Router. According to Learn, components of a library do not depend on a mode; setting the mode on the definition is appropriate for pages and for components that only make sense interactively, like the weekly calendar in K2 2.5.8.
Auto and its rules
InteractiveAuto first renders over the server connection, downloads the runtime and the bundle in the background and uses them on the next visit. Two rules from Learn:
- The mode of a visible component never changes. Auto makes a first decision, and the component stays with it as long as it is on the page.
- Auto prefers the mode of existing interactive components. If a server island is already on the page, an Auto component also starts on the server, so that no second runtime comes into being that does not share the state of the first.
The consequence from K2 2.5.10 still holds: the component has to be able to run in both worlds, with services for the server and for the browser. Auto pays off where the first visit has to be fast and many follow; for the Kladde with its five regular users, it does not.
Pages that must stay static
With global interactivity, there are pages that only work in the cycle of request and response, above all everything with cookies, that is, signing in and out. Such pages get [ExcludeFromInteractiveRouting]:
@page "/internal/sign-in"
@attribute [ExcludeFromInteractiveRouting]
The way to this page leaves interactive navigation: the browser reloads the page completely, so that App renders again on the server and can choose a different mode. App reads with AcceptsInteractiveRouting() whether the page carries the attribute (throwaway project Kladde.Global):
<HeadOutlet @rendermode="PageRenderMode" />
<Routes @rendermode="PageRenderMode" />
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
// Interactive everywhere, except for pages that carry [ExcludeFromInteractiveRouting]: they stay static.
private IComponentRenderMode? PageRenderMode =>
HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
null means "no mode", and because App itself renders statically, the page stays static. Learn warns that null only inherits the mode of the parent and does not force static everywhere; a test checks it (Exercise 3). The Kladde has no global interactivity and does not need the attribute; its sign-in is an endpoint (W8.2.1).
Prerendering: on by default, sometimes off
All interactive modes prerender: the server sends HTML first, then the interactive component takes over. The drawback is in K2 2.5.8: two render passes, loading data twice (solution in W8.1.2). Learn names three things about it:
OnAfterRender{Async}does not run during prerendering, only once the component is interactive.- With interactive navigation inside the app, there is no prerendering.
- A component in the
.Clientproject is prerendered on the server and needs its services there. If a service of the browser is missing, prerendering aborts. Among the ways out, Learn names an abstraction of your own with two implementations (this is howIContactSourceworks in this course) and turning prerendering off.
It is turned off with a mode created with prerender: false. That suits a component that cannot show anything without a browser, such as a map that only a script draws. A shorthand goes once into the project, in AppRenderModes.cs, and Components/_Imports.razor gets @using static Kladde.Web.AppRenderModes:
public static class AppRenderModes
{
public static IComponentRenderMode InteractiveServerWithoutPrerendering { get; } =
new InteractiveServerRenderMode(prerender: false);
}
@page "/internal/map"
@rendermode InteractiveServerWithoutPrerendering
The setting only applies to the topmost interactive mode: if a parent component sets a mode, Blazor ignores the prerender setting of the children. And it costs: the page is empty until the connection is up. Turn prerendering off where it causes errors or does no good, not out of habit.
Where is this running? RendererInfo
RendererInfo.Name is Static, Server, WebAssembly or WebView, RendererInfo.IsInteractive says whether the component responds to clicks, and AssignedRenderMode is the assigned mode or null. The diagnostic component RenderInfo.razor of the throwaway project:
<p role="status" data-testid="render-info">
Running: @RendererInfo.Name, interactive: @(RendererInfo.IsInteractive ? "yes" : "no"),
Mode: @(AssignedRenderMode?.GetType().Name ?? "none")
</p>
During prerendering, IsInteractive is false and the name is Static, even for a component that later runs on the server: "Static", then "Server". So the name does not tell whether a page contains an island; the marker in the HTML or AssignedRenderMode does. Do not leave RenderInfo in the product: it only makes visible what your choice does.
The decision folder
The choice per page belongs in docs/decisions.md. The questions, each from the cheap to the expensive:
| Question | If yes |
|---|---|
| Does the page have to work for everyone, even without script, and is reading enough? | static |
| Does it need cookies or the cycle of request and response? | static; with global interactivity, with [ExcludeFromInteractiveRouting] |
| Does the code access a database or secrets, and are there few users? | Interactive Server |
| Does the component compute a lot locally, do very many people use it at the same time, or should it run without a connection? | Interactive WebAssembly, permissions in the API |
| Should the first visit be fast, do many visits follow, and can both worlds be tested? | Interactive Auto |
For the Kladde:
| Page | Mode | Prerendering | Reason |
|---|---|---|---|
/, /appointment, /privacy |
static | not applicable | work for everyone and without script |
/internal/calendar |
Interactive Server | on, with [PersistentState] |
paging is many small steps; few users, access to the database |
/internal/contacts |
Interactive WebAssembly | on | decision of the course, to teach the way over the API |
| Signing in and out | endpoints | not applicable | cookies belong in the cycle of request and response |
InteractiveAuto |
nowhere | both worlds would have to be checked, without a page asking for it |
And the entry for it, in the pattern of K2 2.5.10:
## Render mode per page
As of: after lesson W8.1.1.
Decision: Public pages are static. /internal/calendar is a server island with prerendering,
/internal/contacts a WebAssembly island (decision of the course W8, not of operations). Auto is not used.
Reasons:
- The calendar reads the database directly and has few users; one connection per tab is cheap there.
- The contact list shows the way over the API, with the permissions in the endpoint.
Decide again if: the number of concurrent tabs determines the server's memory (measurement in W8.3.1),
a page should run offline or a page needs cookies.
"Decide again if" is the most important line: it turns a decision into a condition that someone can check later.
Exercise 1: Think first
What happens, and why? Answer in one sentence before you look it up.
- The page
Contactshas@rendermode InteractiveWebAssemblyand contains<Chart @rendermode="InteractiveServer" />. - A static page writes
<Dialog @rendermode="InteractiveServer"><p>Text</p></Dialog>. - A static page contains
<Counter @rendermode="InteractiveServer" />and after it<Summary @rendermode="InteractiveAuto" />. Where doesSummaryrun? - An Auto component ran on the server on the first visit; the browser has meanwhile downloaded the runtime and the bundle. Does it now run in the browser, without reloading the page?
- An app with
<Routes @rendermode="InteractiveServer" />gets a page that sets a cookie.
Solution 1
- An error at run time (rule 3). Fix:
Chartwithout a mode, then it inherits WebAssembly. - An error at run time:
ChildContentcannot be serialized (rule 4). Fix: a wrapper component that fillsDialogwith the content and gets the mode on the wrapper. - On the server: Auto prefers the mode of existing interactive components.
- No. A visible component never changes its mode; the change happens on the next visit.
- The page has to stay static:
@attribute [ExcludeFromInteractiveRouting], andAppchooses the mode per request withAcceptsInteractiveRouting(). Alternatively, you put the sign-in into an endpoint outside the components, like the Kladde.
Exercise 2: The Kladde's decision folder
For all pages of your Kladde, including those the course has not built yet (such as the customer file from W8.2.2), write one row of the table from the section "The decision folder": mode, prerendering, reason. For every interactive page, add a "Decide again if". Create docs/decisions.md or extend it on a branch docs/render-modes.
How to check: every reason answers one of the five questions, and no page is listed with "Interactive Auto" without naming both worlds that you test.
Solution 2
Like the table above, plus the customer file as a server island with prerendering: a long form, access to the database, few users, and the state of the draft belongs on the server. Every row has to name a condition under which it would become wrong.
Exercise 3: Both sides of global interactivity
Build a throwaway project Kladde.Global (without .Client, only AddInteractiveServerComponents) with the pages / and /internal/sign-in with [ExcludeFromInteractiveRouting] and PageRenderMode in App.razor as above. Write a test with WebApplicationFactory that fetches both pages and checks whether <!--Blazor: is in the response. Predict beforehand: which page has the marker, and what does RendererInfo.Name show in both responses?
Solution 3
using var factory = new WebApplicationFactory<Kladde.Global.Program>();
using HttpClient client = factory.CreateClient();
string home = await client.GetStringAsync("/");
string login = await client.GetStringAsync("/internal/sign-in");
home.ShouldContain("<!--Blazor:");
login.ShouldNotContain("<!--Blazor:");
The home page has the marker, the sign-in page does not. RendererInfo.Name is Static in both responses (Running: Static, interactive: False), because during prerendering even a later server component is not interactive yet. The difference is in the marker, not in the name.
Status of verification. A throwaway project outside the repository was built and run with the .NET SDK 10.0.400 on
net10.0: the Kladde with a.Clientproject (pages for all modes,RenderInfo,InteractiveServerWithoutPrerendering),Kladde.Globaland a test project with xunit, Shouldly, bUnit 2.15.2 andWebApplicationFactory.dotnet testended with exit code 0 (Passed! - Failed: 0, Passed: 20, Skipped: 0, Total: 20). What was seen in the test server's responses: the pages of the modes Server, WebAssembly and Auto carry the markersserver,webassemblyandauto, the static home page none;RendererInfo.Namewas alwaysStaticduring prerendering;/internal/mapwithprerender: falsedelivered the marker but nothing of the content; inKladde.Global, only the home page carried the marker. With bUnit,RendererInfoandAssignedRenderModeran; withoutSetRendererInfo, bUnit aborts when renderingRenderInfowithMissingRendererInfoException. Not run: a browser, the connection, the WebAssembly runtime and Azure. What you read about round trip, memory and download size comes from Microsoft Learn and K2 2.5.10, not from a measurement of this lesson. The throwaway project's packages had version 10.0.11, for which NuGet reported security warnings (NU1903); in a real project, take the current patch version. Open: whether the switch from a static to an interactive root with enhanced navigation works without a reload, Learn does not describe.
What you can do now
- You compare the modes by round trip, connection and download size.
- You set the mode on the component, on the page or for the whole app and name the four rules of passing it down.
- You predict what
InteractiveAutodoes and know that it never changes the mode of a visible component. - You exempt pages from global interactivity with
[ExcludeFromInteractiveRouting]and turn prerendering off where it does no good. - You record the choice per page in
docs/decisions.mdwith a condition for deciding again.
The Kladde: docs/decisions.md names mode, prerendering and reason for every page. Next comes the state that can get lost during prerendering and when the connection drops.