Jede Seite interaktiv auf dem Server, weil es bequem war, und später stehen tausend offene Verbindungen im Speicher. Oder jede Seite in WebAssembly, und der erste Besuch lädt Megabyte, die eine Kontaktliste nie gebraucht hätte. In dieser Lektion wird die Wahl des Render-Modus zu einer Entscheidung mit Kosten, die Sie begründen und aufschreiben, statt zu einer Voreinstellung, die Sie erben.
Wo die Kladde steht
Die Lektion setzt K2 voraus, vor allem 2.5.1 (die Modi als Einführung), 2.5.8 (die erste Server-Insel) und 2.5.10 (die WebAssembly-Insel, InteractiveAuto kurz, der erste Eintrag in docs/entscheidungen.md). Die Kladde besteht aus src/Kladde.Web (Blazor Web App, .NET 10) und src/Kladde.Web.Client; die öffentlichen Seiten sind statisch, der interne Bereich unter /intern hat Inseln. Behalten Sie Ihre Namen und übertragen Sie die Schritte.
Eine Entscheidung dieses Kurses weicht absichtlich von K2 ab: K2 2.5.10 schloss WebAssembly für die Kladde aus. Ab hier bekommt die Kontaktliste unter /intern/kontakte eine WebAssembly-Insel, weil W8.2.1 und W8.3.1 beide Wege brauchen: den über die Verbindung und den über die API. Für fünf Menschen im Büro wäre die Server-Insel auch hier billiger, und die Entscheidungsmappe sagt das ausdrücklich.
Vier Modi, drei Kosten
Was die Modi sind, steht in K2 2.5.1; hier zählt, was jeder kostet. Es gibt drei Posten, und jeder trifft einen anderen Modus.
Der Rundlauf. In einer Server-Insel reist jedes Ereignis über das Netz zum Server, die Komponente rendert dort neu, und die Änderungen am DOM reisen zurück. Die Antwortzeit der Oberfläche ist die Latenz der Verbindung plus die Rechenzeit. Learn empfiehlt eine dauerhafte Oberflächenlatenz von höchstens 250 ms. Bei einer WebAssembly-Insel kostet ein Klick keinen Rundlauf, aber jeder Zugriff auf Daten ist eine Anfrage an die API.
Die Verbindung. Eine Server-Insel hält für jeden offenen Browser-Tab einen Schaltkreis (circuit) samt Zustand im Speicher. Learn nennt rund 250 KB für eine minimale Hello-World-App und rechnet für 5.000 gleichzeitige Nutzende mindestens 1,3 GB ein, etwa 273 KB je Person; die wirkliche Größe hängt vom Zustand Ihrer Komponenten ab und ist zu messen. Arbeitsspeicher geht bei einem einzelnen Server laut Learn meist zuerst aus. Ein Schaltkreis entsteht mit der ersten Server-Komponente einer Seite und endet, wenn keine mehr auf ihr steht. Reißt die Verbindung ab, hält der Server ihn eine Weile bereit: laut CircuitOptions voreingestellt höchstens 100 getrennte Schaltkreise und 3 Minuten.
Das Laden. Eine WebAssembly-Insel lädt beim ersten Besuch Laufzeit und Bündel und legt sie im Cache ab. K2 2.5.10 hat am Spike der Kladde rund 2,3 MB gemessen; Ihre Zahl hängt von Ihrem Code ab und wird in W8.3.1 verkleinert. Dafür beantwortet der Server nur noch API-Anfragen.
| Frage | Statisch | Interactive Server | Interactive WebAssembly |
|---|---|---|---|
| Rundlauf je Ereignis | keiner (neue Seite) | ja, Netz plus Rechenzeit | nein; Daten über die API |
| Kosten auf dem Server | eine Anfrage | Speicher und eine Verbindung je Tab | nur die Anfragen an die API |
| Kosten beim Laden | nur HTML | blazor.web.js und die Verbindung |
dazu Laufzeit und Bündel |
| Zugriff auf Daten | direkt aus dem Dienst | direkt aus dem Dienst | nur über eine API |
| Was der Browser sieht | HTML | HTML | HTML, der C#-Code und der vorgerenderte Zustand |
InteractiveAuto ist keine vierte Spalte, sondern die Wahl zwischen den beiden rechten: erst Server, dann WebAssembly, mit den Kosten beider.
Wo die Interaktivität festgelegt wird
Der Standard ist statisch. Den Modus legen Sie an drei Orten fest.
An der Komponente beim Benutzen (@rendermode als Attribut): Die Eltern entscheiden, dieselbe Komponente kann anderswo statisch bleiben.
<Dialog @rendermode="InteractiveServer" />
An der Definition (@rendermode als Direktive): Sie gilt für jede Benutzung und ist bei Seiten der übliche Weg. Die Direktive verlangt eine statische Instanz.
@page "/intern/kalender"
@rendermode InteractiveServer
Für die ganze App: am obersten interaktiven Bauteil, das keine Wurzelkomponente ist, in der Vorlage Routes in App.razor. Der Router gibt den Modus an die Seiten weiter, HeadOutlet bekommt denselben. Die Wurzel App selbst kann nicht interaktiv sein.
<HeadOutlet @rendermode="InteractiveServer" />
<Routes @rendermode="InteractiveServer" />
dotnet new blazor mit der Option -ai (--all-interactive) legt eine App so an. Wählen Sie für die ganze App WebAssembly oder Auto, müssen Layout, Seiten und Routes in das Projekt .Client umziehen. Bei einer App mit vielen statischen Seiten wie der Kladde bleibt es deshalb bei Inseln: Eine Zeile je Insel, alles andere unberührt.
Die Registrierung in Program.cs für beide Welten:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Kladde.Web.Client.RenderInfo).Assembly);
MapStaticAssets() muss vor AddInteractiveWebAssemblyRenderMode() stehen; ohne sie schrieb das Wegwerfprojekt dieser Lektion beim Start die Warnung „Mapped static asset endpoints not found“. AddAdditionalAssemblies nennt dem Server das Projekt .Client.
Die Regeln der Weitergabe
- Ohne Angabe erbt eine Komponente den Modus ihrer Eltern; ohne jede Angabe ist er statisch.
- Geschwister dürfen verschiedene interaktive Modi haben.
- Ein Kind kann nicht in einen anderen interaktiven Modus wechseln als seine Eltern. Eine Server-Komponente unter einer WebAssembly-Komponente endet zur Laufzeit mit „Cannot create a component of type … because its render mode … is not supported by Interactive Server rendering“.
- Parameter von einer statischen Elternkomponente an eine interaktive Kindkomponente müssen sich nach JSON serialisieren lassen. Ein
RenderFragmentwieChildContentgeht nicht („… the parameter is of the delegate type … which is arbitrary code and cannot be serialized“).
Gegen die vierte Regel hilft eine Hüllkomponente ohne den Parameter, der die Eltern den Modus geben; so macht es die Vorlage mit Routes um den Router. Komponenten einer Bibliothek hängen laut Learn nicht an einem Modus; den Modus an der Definition festzulegen ist für Seiten angebracht und für Komponenten, die nur interaktiv Sinn haben, wie der Wochenkalender in K2 2.5.8.
Auto und seine Regeln
InteractiveAuto rendert zuerst über die Serververbindung, lädt Laufzeit und Bündel im Hintergrund und nutzt sie beim nächsten Besuch. Zwei Regeln aus Learn:
- Der Modus einer sichtbaren Komponente wechselt nie. Auto trifft eine erste Entscheidung, und dabei bleibt die Komponente, solange sie auf der Seite steht.
- Auto bevorzugt den Modus vorhandener interaktiver Komponenten. Steht auf der Seite schon eine Server-Insel, startet eine Auto-Komponente auch auf dem Server, damit keine zweite Laufzeit entsteht, die den Zustand der ersten nicht teilt.
Die Folge aus K2 2.5.10 gilt weiter: Die Komponente muss beide Welten können, mit Diensten für Server und Browser. Auto lohnt sich, wo der erste Besuch schnell sein muss und viele folgen; für die Kladde mit ihren fünf Stammnutzenden nicht.
Seiten, die statisch bleiben müssen
Bei globaler Interaktivität gibt es Seiten, die nur im Zyklus von Anfrage und Antwort funktionieren, vor allem alles mit Cookies, also An- und Abmeldung. Solche Seiten bekommen [ExcludeFromInteractiveRouting]:
@page "/intern/anmelden"
@attribute [ExcludeFromInteractiveRouting]
Der Weg zu dieser Seite verlässt die interaktive Navigation: Der Browser lädt die Seite vollständig neu, damit App auf dem Server neu rendert und einen anderen Modus wählen kann. App liest mit AcceptsInteractiveRouting(), ob die Seite das Attribut trägt (Wegwerfprojekt 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 heißt „kein Modus“, und weil App selbst statisch rendert, bleibt die Seite statisch. Learn warnt, dass null nur den Modus der Eltern erbt und nicht überall statisch erzwingt; ein Test prüft es (Übung 3). Die Kladde hat keine globale Interaktivität und braucht das Attribut nicht; ihre Anmeldung ist ein Endpunkt (W8.2.1).
Vorrendern: standardmäßig an, manchmal aus
Alle interaktiven Modi rendern vor: Der Server schickt zuerst HTML, danach übernimmt die interaktive Komponente. Der Nachteil steht in K2 2.5.8: zwei Renderläufe, zweimal Daten laden (Lösung in W8.1.2). Learn nennt drei Dinge dazu:
OnAfterRender{Async}läuft beim Vorrendern nicht, erst wenn die Komponente interaktiv ist.- Bei interaktiver Navigation innerhalb der App gibt es kein Vorrendern.
- Eine Komponente im Projekt
.Clientwird auf dem Server vorgerendert und braucht dort ihre Dienste. Fehlt ein Dienst des Browsers, bricht das Vorrendern ab. Learn nennt als Auswege unter anderem eine eigene Abstraktion mit zwei Umsetzungen (so gehtIContactSourcein diesem Kurs) und das Abschalten des Vorrenderns.
Abgeschaltet wird es mit einem Modus, der mit prerender: false entsteht. Das passt zu einer Komponente, die ohne Browser nichts zeigen kann, etwa einer Karte, die nur ein Skript zeichnet. Eine Kurzform steht einmal im Projekt, in AppRenderModes.cs, und Components/_Imports.razor bekommt @using static Kladde.Web.AppRenderModes:
public static class AppRenderModes
{
public static IComponentRenderMode InteractiveServerWithoutPrerendering { get; } =
new InteractiveServerRenderMode(prerender: false);
}
@page "/intern/karte"
@rendermode InteractiveServerWithoutPrerendering
Die Einstellung gilt nur für den obersten interaktiven Modus: Setzt eine Elternkomponente einen Modus, ignoriert Blazor die Vorrender-Einstellung der Kinder. Und sie kostet: Die Seite ist bis zur Verbindung leer. Schalten Sie das Vorrendern ab, wo es Fehler macht oder nichts nützt, nicht aus Gewohnheit.
Wo läuft das hier? RendererInfo
RendererInfo.Name ist Static, Server, WebAssembly oder WebView, RendererInfo.IsInteractive sagt, ob die Komponente auf Klicks antwortet, und AssignedRenderMode ist der zugewiesene Modus oder null. Die Diagnosekomponente RenderInfo.razor des Wegwerfprojekts:
<p role="status" data-testid="render-info">
Läuft: @RendererInfo.Name, interaktiv: @(RendererInfo.IsInteractive ? "ja" : "nein"),
Modus: @(AssignedRenderMode?.GetType().Name ?? "keiner")
</p>
Beim Vorrendern ist IsInteractive falsch, und der Name ist Static, auch für eine Komponente, die später auf dem Server läuft: erst Static, dann Server. Der Name unterscheidet also nicht, ob eine Seite eine Insel enthält; das sagt die Markierung im HTML oder AssignedRenderMode. Lassen Sie RenderInfo nicht im Produkt: Sie macht nur sichtbar, was Ihre Wahl bewirkt.
Die Entscheidungsmappe
Die Wahl je Seite gehört in docs/entscheidungen.md. Die Fragen, jeweils vom Billigen zum Teuren:
| Frage | Wenn ja |
|---|---|
| Muss die Seite für alle gehen, auch ohne Skript, und lesen reicht? | statisch |
| Braucht sie Cookies oder den Zyklus von Anfrage und Antwort? | statisch; bei globaler Interaktivität mit [ExcludeFromInteractiveRouting] |
| Greift der Code auf Datenbank oder Geheimnisse zu, und sind es wenige Nutzende? | Interactive Server |
| Rechnet die Komponente viel lokal, nutzen sehr viele Menschen sie gleichzeitig, oder soll sie ohne Verbindung laufen? | Interactive WebAssembly, Rechte in der API |
| Soll der erste Besuch schnell sein, folgen viele Besuche, und sind beide Welten testbar? | Interactive Auto |
Für die Kladde:
| Seite | Modus | Vorrendern | Grund |
|---|---|---|---|
/, /termin, /datenschutz |
statisch | nicht zutreffend | gehen für alle und ohne Skript |
/intern/kalender |
Interactive Server | an, mit [PersistentState] |
Blättern sind viele kleine Schritte; wenige Nutzende, Zugriff auf die Datenbank |
/intern/kontakte |
Interactive WebAssembly | an | Entscheidung des Kurses, um den Weg über die API zu lehren |
| An- und Abmelden | Endpunkte | nicht zutreffend | Cookies gehören in den Zyklus von Anfrage und Antwort |
InteractiveAuto |
nirgends | beide Welten wären zu prüfen, ohne dass eine Seite es verlangt |
Und der Eintrag dazu, im Muster von K2 2.5.10:
## Render-Modus je Seite
Stand: nach Lektion W8.1.1.
Entscheidung: Öffentliche Seiten sind statisch. /intern/kalender ist eine Server-Insel mit Vorrendern,
/intern/kontakte eine WebAssembly-Insel (Entscheidung des Kurses W8, nicht des Betriebs). Auto wird nicht eingesetzt.
Gründe:
- Der Kalender liest die Datenbank direkt und hat wenige Nutzende; eine Verbindung je Tab ist dort billig.
- Die Kontaktliste zeigt den Weg über die API, mit den Rechten im Endpunkt.
Neu entscheiden, wenn: die Zahl gleichzeitiger Tabs den Speicher des Servers bestimmt (Messung in W8.3.1),
eine Seite offline laufen soll oder eine Seite Cookies braucht.
Die Angabe „Neu entscheiden, wenn“ ist die wichtigste: Sie macht aus einer Entscheidung eine Bedingung, die jemand später prüfen kann.
Übung 1: Erst denken
Was geschieht, und warum? Antworten Sie in einem Satz, bevor Sie nachlesen.
- Die Seite
Kontaktehat@rendermode InteractiveWebAssemblyund enthält<Chart @rendermode="InteractiveServer" />. - Eine statische Seite schreibt
<Dialog @rendermode="InteractiveServer"><p>Text</p></Dialog>. - Eine statische Seite enthält
<Counter @rendermode="InteractiveServer" />und danach<Summary @rendermode="InteractiveAuto" />. Wo läuftSummary? - Eine Auto-Komponente lief beim ersten Besuch auf dem Server; der Browser hat Laufzeit und Bündel inzwischen geladen. Läuft sie jetzt, ohne Neuladen der Seite, im Browser?
- Eine App mit
<Routes @rendermode="InteractiveServer" />bekommt eine Seite, die ein Cookie setzt.
Lösung 1
- Ein Fehler zur Laufzeit (Regel 3). Fix:
Chartohne Modus, dann erbt es WebAssembly. - Ein Fehler zur Laufzeit:
ChildContentlässt sich nicht serialisieren (Regel 4). Fix: eine Hüllkomponente, dieDialogmit dem Inhalt füllt und den Modus an der Hülle bekommt. - Auf dem Server: Auto bevorzugt den Modus vorhandener interaktiver Komponenten.
- Nein. Eine sichtbare Komponente wechselt den Modus nie; der Wechsel geschieht beim nächsten Besuch.
- Die Seite muss statisch bleiben:
@attribute [ExcludeFromInteractiveRouting], undAppwählt mitAcceptsInteractiveRouting()den Modus je Anfrage. Alternativ setzen Sie die Anmeldung als Endpunkt außerhalb der Komponenten, wie die Kladde.
Übung 2: Die Entscheidungsmappe der Kladde
Schreiben Sie für alle Seiten Ihrer Kladde, auch die, die der Kurs noch nicht gebaut hat (etwa die Kundenakte aus W8.2.2), eine Zeile der Tabelle aus dem Abschnitt „Die Entscheidungsmappe“: Modus, Vorrendern, Grund. Für jede interaktive Seite kommt ein „Neu entscheiden, wenn“ dazu. Legen Sie docs/entscheidungen.md an oder ergänzen Sie es auf einem Branch docs/render-modi.
So prüfen Sie: Jeder Grund beantwortet eine der fünf Fragen, und keine Seite steht mit „Interactive Auto“, ohne dass beide Welten genannt sind, die Sie testen.
Lösung 2
Wie die Tabelle oben, dazu die Kundenakte als Server-Insel mit Vorrendern: ein langes Formular, Zugriff auf die Datenbank, wenige Nutzende, der Zustand des Entwurfs gehört auf den Server. Jede Zeile muss eine Bedingung nennen, unter der sie falsch würde.
Übung 3: Beide Seiten der globalen Interaktivität
Bauen Sie ein Wegwerfprojekt Kladde.Global (ohne .Client, nur AddInteractiveServerComponents) mit den Seiten / und /intern/anmelden mit [ExcludeFromInteractiveRouting] und PageRenderMode in App.razor wie oben. Schreiben Sie einen Test mit WebApplicationFactory, der beide Seiten abruft und prüft, ob <!--Blazor: in der Antwort steht. Sagen Sie vorher voraus: Welche Seite hat die Markierung, und was zeigt RendererInfo.Name in beiden Antworten?
Lösung 3
using var factory = new WebApplicationFactory<Kladde.Global.Program>();
using HttpClient client = factory.CreateClient();
string home = await client.GetStringAsync("/");
string login = await client.GetStringAsync("/intern/anmelden");
home.ShouldContain("<!--Blazor:");
login.ShouldNotContain("<!--Blazor:");
Die Startseite hat die Markierung, die Anmeldeseite nicht. RendererInfo.Name ist in beiden Antworten Static (Läuft: Static, interaktiv: False), denn beim Vorrendern ist auch eine spätere Server-Komponente noch nicht interaktiv. Der Unterschied steht in der Markierung, nicht im Namen.
Stand der Prüfung. Gebaut und ausgeführt wurde ein Wegwerfprojekt außerhalb des Repositorys mit dem .NET-SDK 10.0.400 auf
net10.0: die Kladde mit Projekt.Client(Seiten für alle Modi,RenderInfo,InteractiveServerWithoutPrerendering),Kladde.Globalund ein Testprojekt mit xunit, Shouldly, bUnit 2.15.2 undWebApplicationFactory.dotnet testendete mit dem Exit-Code 0 (Bestanden! : Fehler: 0, erfolgreich: 20, übersprungen: 0, gesamt: 20). Gesehen wurde in den Antworten des Testservers: Die Seiten der Modi Server, WebAssembly und Auto tragen die Markierungenserver,webassemblyundauto, die statische Startseite keine;RendererInfo.Namewar beim Vorrendern immerStatic;/intern/kartemitprerender: falselieferte die Markierung, aber nichts vom Inhalt; inKladde.Globaltrug nur die Startseite die Markierung. Mit bUnit liefenRendererInfoundAssignedRenderMode; ohneSetRendererInfobricht bUnit beim Rendern vonRenderInfomitMissingRendererInfoExceptionab. Nicht ausgeführt wurden ein Browser, die Verbindung, die WebAssembly-Laufzeit und Azure: Was Sie über Rundlauf, Speicher und Ladegröße lesen, stammt aus Microsoft Learn und K2 2.5.10, nicht aus einer Messung dieser Lektion. Die Pakete des Wegwerfprojekts hatten die Version 10.0.11, für die NuGet Sicherheitswarnungen (NU1903) meldete; nehmen Sie in einem echten Projekt die aktuelle Patch-Version. Offen: Ob der Wechsel von einer statischen zu einer interaktiven Wurzel per Enhanced Navigation ohne Neuladen gelingt, stellt Learn nicht dar.
Was Sie jetzt können
- Sie vergleichen die Modi nach Rundlauf, Verbindung und Ladegröße.
- Sie legen den Modus an der Komponente, an der Seite oder für die ganze App fest und nennen die vier Regeln der Weitergabe.
- Sie sagen vorher, was
InteractiveAutotut, und wissen, dass es den Modus einer sichtbaren Komponente nie wechselt. - Sie nehmen Seiten mit
[ExcludeFromInteractiveRouting]aus der globalen Interaktivität aus und schalten das Vorrendern ab, wo es nichts nützt. - Sie halten die Wahl je Seite in
docs/entscheidungen.mdmit einer Bedingung für die Neuentscheidung fest.
Die Kladde: docs/entscheidungen.md nennt für jede Seite Modus, Vorrendern und Grund. Als Nächstes geht es um den Zustand, der beim Vorrendern und beim Abbruch der Verbindung verloren gehen kann.