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.

Razor
<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.

Razor
@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.

Razor
<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:

C#
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

  1. Ohne Angabe erbt eine Komponente den Modus ihrer Eltern; ohne jede Angabe ist er statisch.
  2. Geschwister dürfen verschiedene interaktive Modi haben.
  3. 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“.
  4. Parameter von einer statischen Elternkomponente an eine interaktive Kindkomponente müssen sich nach JSON serialisieren lassen. Ein RenderFragment wie ChildContent geht 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]:

Razor
@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):

Razor
<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 .Client wird 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 geht IContactSource in 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:

C#
public static class AppRenderModes
{
    public static IComponentRenderMode InteractiveServerWithoutPrerendering { get; } =
        new InteractiveServerRenderMode(prerender: false);
}
Razor
@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:

Razor
<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.

  1. Die Seite Kontakte hat @rendermode InteractiveWebAssembly und enthält <Chart @rendermode="InteractiveServer" />.
  2. Eine statische Seite schreibt <Dialog @rendermode="InteractiveServer"><p>Text</p></Dialog>.
  3. Eine statische Seite enthält <Counter @rendermode="InteractiveServer" /> und danach <Summary @rendermode="InteractiveAuto" />. Wo läuft Summary?
  4. 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?
  5. Eine App mit <Routes @rendermode="InteractiveServer" /> bekommt eine Seite, die ein Cookie setzt.

Lösung 1

  1. Ein Fehler zur Laufzeit (Regel 3). Fix: Chart ohne Modus, dann erbt es WebAssembly.
  2. Ein Fehler zur Laufzeit: ChildContent lässt sich nicht serialisieren (Regel 4). Fix: eine Hüllkomponente, die Dialog mit dem Inhalt füllt und den Modus an der Hülle bekommt.
  3. Auf dem Server: Auto bevorzugt den Modus vorhandener interaktiver Komponenten.
  4. Nein. Eine sichtbare Komponente wechselt den Modus nie; der Wechsel geschieht beim nächsten Besuch.
  5. Die Seite muss statisch bleiben: @attribute [ExcludeFromInteractiveRouting], und App wählt mit AcceptsInteractiveRouting() 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

C#
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.Global und ein Testprojekt mit xunit, Shouldly, bUnit 2.15.2 und WebApplicationFactory. dotnet test endete 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 Markierungen server, webassembly und auto, die statische Startseite keine; RendererInfo.Name war beim Vorrendern immer Static; /intern/karte mit prerender: false lieferte die Markierung, aber nichts vom Inhalt; in Kladde.Global trug nur die Startseite die Markierung. Mit bUnit liefen RendererInfo und AssignedRenderMode; ohne SetRendererInfo bricht bUnit beim Rendern von RenderInfo mit MissingRendererInfoException ab. 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 InteractiveAuto tut, 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.md mit 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.

Weiterlesen