Blazor on .NET 10 in 2026 — Mastering Render Modes, Stream Rendering, and Enhanced Navigation for Full-stack C#

Posted on: 4/17/2026 1:10:42 AM

1. Blazor on .NET 10 — When Frontend Is No Longer JavaScript-Only Territory

For nearly two decades, modern frontend has been practically synonymous with JavaScript or TypeScript. Every framework we talk about — React, Vue, Angular, Svelte, Solid — runs on the browser's JavaScript runtime, and any .NET full-stack engineer essentially has to maintain two parallel codebases: C# for the backend and JS/TS for the frontend. Blazor arrived in 2018 with a simple promise: write your UI in C# and Razor, run it in both the browser and on the server, sharing the same model, the same DTOs, the same validation as the backend. After many "experimental-feeling" releases, .NET 8 introduced the Blazor United model: one single Blazor project that can mix different render modes. By .NET 10 LTS (GA in November 2025), the toolchain is finally mature enough to put Blazor into large-scale production, with support extending to 2028.

This article is a hands-on handbook for architects and senior engineers caught between two choices: stick with a classic JavaScript SPA (Vue/React plus ASP.NET Core API), or move to full-stack C# with Blazor on .NET 10. We'll walk through each Render Mode, Stream Rendering, Enhanced Navigation, AOT WebAssembly, state persistence, security, performance tuning, and how to migrate a Razor Pages or MVC project piece by piece into Blazor without rewriting the whole app.

4Render Modes
~1.8MBWASM app size after AOT + trimming
60msAverage Server-mode round-trip
3 yearsLTS support for .NET 10 (until 11/2028)

2. From Blazor Server 2018 to Blazor United 2026 — A Brief Timeline

2018 — Blazor Server preview
The first model: UI rendered on the server, every interaction sent a diff over SignalR. Smooth DX but dependent on a WebSocket connection.
2020 — Blazor WebAssembly GA
The Mono/.NET runtime was compiled to WebAssembly, running entirely in the browser. Heavy app (a few MB) but no SignalR needed.
2022 — Blazor Hybrid (.NET MAUI)
Reuse Blazor components in native desktop/mobile apps via a WebView.
2023 — .NET 8 Blazor United
First release that can mix Static SSR, Server, WebAssembly, and Auto in the same project. The concept of Render Mode appears, replacing the decade-old "either Server or WASM" model.
2024 — .NET 9
Reconnect UX upgrade (auto banner on Server-mode disconnect), more stable Enhanced Navigation, Jiterpreter accelerates WASM hot paths.
11/2025 — .NET 10 LTS GA
QuickGrid gains virtual scrolling, ValidateEditContextAsync standardises async validation, PersistentComponentState survives Enhanced Navigation, WASM bootstrap is ~20% lighter thanks to WebCIL partitioning. Official support for sequential hydration with Stream Rendering.
Q1 2026 — ecosystem
MudBlazor 8, FluentUI Blazor 5, Radzen 6 ship for .NET 10; YARP 2 makes the Blazor BFF topology slicker; large teams (Visual Studio, parts of the Azure Portal) run Blazor at millions-of-users-a-day scale in production.

3. Render Modes — Four Rendering Styles, One Component Model

The biggest stumbling block for newcomers to Blazor United is Render Mode. The same .razor file can render in four different ways depending on how you declare @rendermode at the call site. This isn't magic — the framework compiles the component into two kinds of artefact: one that runs on the server to render static HTML and hold state, and one that can be shipped to the browser to "hydrate" into an interactive UI.

graph TB
    SRC["MyPage.razor"] --> COMPILE["Razor compiler"]
    COMPILE --> SSR["Static SSR HTML"]
    COMPILE --> SRV["Blazor Server circuit"]
    COMPILE --> WASM["WebAssembly bundle"]
    SSR --> HTML["Initial HTML sent to browser"]
    HTML --> HYDRATE{"rendermode?"}
    HYDRATE -->|None| STATIC["Read-only UI, like MVC"]
    HYDRATE -->|InteractiveServer| SIGNALR["SignalR circuit, server holds state"]
    HYDRATE -->|InteractiveWebAssembly| DOTNETWASM["Load .NET WASM runtime, run in browser"]
    HYDRATE -->|InteractiveAuto| AUTO["First visit Server, prefetch WASM for next time"]
One component, four ways to hydrate
Render ModeWhere C# logic runsNeeds SignalR?Initial downloadTypical use case
Static SSR (default)Server, once per requestNoHTML onlyMarketing pages, landing, SEO pages, blogs, product lists
InteractiveServerServer (circuit keeps state in RAM)Yes (WebSocket)~90KB blazor.web.jsInternal admin dashboards, apps needing low-latency DB access, keeping code on the server
InteractiveWebAssemblyBrowser (WASM)No~1.5–2MB runtime + appPWAs, offline-first apps, rich local interaction, light games, canvas
InteractiveAutoFirst visit: Server; once WASM is cached, subsequent visits run in the browserYes on first visit, no afterwardsSmall first, large after prefetchPublic apps with many dynamic pages, wanting fast first experience and good scale

3.1 Declaring rendermode on a component

@* Counter.razor *@
@page "/counter"
@rendermode InteractiveAuto

<h3>Counter</h3>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="Increment">Click me</button>

@code {
    private int currentCount;
    private void Increment() => currentCount++;
}

For a component that should render statically, you simply omit @rendermode. To make a small part of a static page interactive, wrap it in a component with its own @rendermode — this is the mix strength that JS frameworks only achieve with "island architecture" (Astro, Fresh).

How to pick a Render Mode

Default to Static SSR. Only promote to Server or WebAssembly when a component truly needs interactivity. Every InteractiveServer component adds SignalR load; every InteractiveWebAssembly component pulls the runtime. Splitting SEO pages and app pages into two different render boundaries yields a far better bundle size and TTFB.

4. Static SSR — The New Soul of Blazor United

Many people hear "Blazor" and think immediately of "an SPA running C# in the browser". In fact, since .NET 8, the default mode is Static SSR: a component renders once on the server, the result is HTML sent to the browser, no Blazor runtime, no SignalR attached. Just like Razor Pages but with reusable component syntax.

The big implications of Static SSR:

  • No runtime cost on the client — lightweight pages, solid Core Web Vitals, SEO-friendly.
  • Component reuse — the same Blazor component can serve a public page (Static) and an admin page (InteractiveServer).
  • Native HTTP form handlingEditForm with [SupplyParameterFromForm] is equivalent to an MVC action and works even without JS.
  • Stream Rendering — HTML can be pushed progressively to the browser instead of waiting for everything before flushing (see below).
@* Products.razor *@
@page "/products"
@attribute [StreamRendering]
@inject IProductRepository Repo

<h1>Featured products</h1>

@if (products is null)
{
    <p>Loading…</p>
}
else
{
    <ul>
        @foreach (var p in products)
        {
            <li>@p.Name — @p.Price.ToString("C0")</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Product>? products;

    protected override async Task OnInitializedAsync()
    {
        products = await Repo.GetFeaturedAsync();
    }
}

This snippet needs no JS, no SignalR, no WASM. With [StreamRendering], Blazor sends the "Loading…" HTML first so the browser can start painting, then flushes the product list once OnInitializedAsync completes. This is how .NET 10 gives traditional Razor Pages a "snappy" SPA-like feel.

5. Stream Rendering — HTML Arrives in Pieces, No More Staring at Spinners

Stream Rendering isn't new — React 18 has Suspense + streaming SSR, Vue 3 has <Suspense>, Nuxt has Partial Hydration. What's interesting is that Blazor on .NET 10 does this at component granularity with a single [StreamRendering] attribute, working in both Static SSR and the first load of InteractiveServer.

sequenceDiagram
    participant B as Browser
    participant S as ASP.NET Core
    participant DB as Database
    B->>S: GET /products
    S->>B: Frame HTML (header, <h1>, "Loading…" placeholder)
    Note over B: Browser paints immediately, TTFB ~30ms
    S->>DB: SELECT TOP 50 products
    DB-->>S: rows
    S->>B: List HTML fragment (as <template blazor-ssr-stream>)
    Note over B: Blazor runtime inserts it at the right spot
    S->>B: End of stream
Stream Rendering — HTML flows in stages within a single response

In practice, the most valuable outcome is that Largest Contentful Paint (LCP) improves noticeably because the header and above-the-fold content appear right away, while slow data (DB queries, API calls) waits below. E-commerce teams that moved from classic Razor Pages to Static SSR + Stream Rendering report dropping LCP from 2.4s to about 900ms without rewriting business logic.

Stream Rendering gotcha

Stream rendering requires the server to send Transfer-Encoding: chunked. Some older reverse proxies (NGINX without proxy_buffering off, older Azure Front Door) buffer the full response and defeat the feature. Always verify end-to-end with real traces, not just localhost.

6. Enhanced Navigation & Enhanced Form — SPA Feel Without a SPA

When you click an <a> in a Blazor Static SSR page, Enhanced Navigation doesn't reload the whole page. Instead, a tiny (~11KB) Blazor JS bundle fetches the new page, diffs the DOM, and patches only the changed parts. Users get the SPA experience: no white flash, no lost scroll position, state preserved in any Interactive components on the page. But the backend is still plain Razor/Blazor rendering HTML — no reducers, no JS router, no state machine.

Enhanced Form works the same way: submitting a form doesn't reload the page; Blazor updates only the rest of the body. Combined with EditForm and model binding, you get the classic POST-REDIRECT-GET flow, durable and simple, yet with the soft feel of a SPA.

@* Layout with enhanced nav *@
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
</Router>

<a href="/products" data-enhance-nav="true">Products</a>

On .NET 10, data-enhance-nav="true" is the default — all you need is for the page to sit inside a layout wrapped in <Router> and it activates automatically. To force a full-page reload on a link (e.g. on logout), use data-enhance-nav="false".

7. WebAssembly AOT, Trimming, and WebCIL — Getting the Bundle to a Reasonable Size

Early Blazor WebAssembly was criticised for its bundle size. Even a "Hello World" could weigh 3–4 MB. .NET 10 closes most of that gap with three main techniques:

  1. Trimming — compiles and keeps only the code actually used. IL Linker and ILC strip unused reflection paths and shrink the BCL.
  2. WebCIL — packs assemblies into the .webcil format (a pseudo-ELF wrapper) so they bypass MIME filters on proxies/CDNs that block .dll; it also compresses better under Brotli.
  3. AOT (Ahead-of-Time) — compiles C# IL into native WASM instead of using the IL interpreter. Apps run 4–6x faster on hot paths, at the cost of a larger bundle (2–3 MB extra).
<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <PublishTrimmed>true</PublishTrimmed>
  <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
  <BlazorEnableCompression>true</BlazorEnableCompression>
</PropertyGroup>

With the new WasmStripILAfterAOT in .NET 10, the toolchain discards the original IL after emitting native WASM, shaving another ~20% off the bundle. In practice, publish + Brotli for a mid-sized business app (MudBlazor + EF client DTOs) typically lands at 1.8–2.4MB — no longer an alien number next to a mega React SPA.

7.1 Jiterpreter — The Middle Ground Between Interpreter and AOT

Not every app tolerates AOT (which adds minutes to build time). Jiterpreter, on by default since .NET 9, compiles hot IL opcodes on the fly while the app runs. It's "partial JIT" inside the WASM runtime — users feel almost no difference from AOT on typical UI, while the build stays as fast as the IL interpreter.

8. State Management — Persistent State and Enhanced Navigation

Blazor has no Redux/Pinia equivalent as a standard. In return, everything you need is DI + component state + PersistentComponentState. Consider the classic scenario: a user opens a product detail page, the server-side prerender has already queried the DB and rendered the HTML; when the component becomes Interactive (Server or WASM), we don't want to re-query the DB/API.

@inject PersistentComponentState ApplicationState

@code {
    private Product? product;
    private PersistingComponentStateSubscription subscription;

    protected override async Task OnInitializedAsync()
    {
        subscription = ApplicationState.RegisterOnPersisting(PersistProduct);

        if (!ApplicationState.TryTakeFromJson<Product>("product", out var fromCache) || fromCache is null)
        {
            product = await Repo.GetAsync(Id);
        }
        else
        {
            product = fromCache;
        }
    }

    private Task PersistProduct()
    {
        ApplicationState.PersistAsJson("product", product);
        return Task.CompletedTask;
    }

    public void Dispose() => subscription.Dispose();
}

What's new in .NET 10: PersistentComponentState now survives Enhanced Navigation hops (previously it only lived between prerender and first render of the same page). That turns it into a near "client cache" for static pages.

8.1 Service-scoped state

With InteractiveServer, each circuit = one DI scope, so you can register a service with Scoped lifetime and share state across components within the same browser tab:

public class CartState
{
    public event Action? Changed;
    private readonly List<CartItem> _items = new();
    public IReadOnlyList<CartItem> Items => _items;
    public void Add(CartItem i) { _items.Add(i); Changed?.Invoke(); }
}

builder.Services.AddScoped<CartState>();

Components subscribe to the Changed event in OnInitialized and call StateHasChanged when it fires. Simple, no external library, no reducer boilerplate.

9. Authentication, Authorization, and BFF for Blazor United

The perennial headache when adopting Blazor United is: cookie auth works for Static SSR pages but doesn't automatically transfer to WASM; JWT is a natural fit for WASM but awkward to wire into Server-rendered pages. The 2026 answer is the BFF pattern (Backend-for-Frontend): the httpOnly cookie lives on the server, the client only talks to its own origin, and every third-party API request is proxied through the BFF.

graph LR
    USER["Browser"] -- httpOnly cookie --> BFF["ASP.NET Core BFF
Blazor + YARP"] BFF -- AddIdentity --> IDP["OIDC Provider
(Keycloak, Auth0, Entra)"] BFF -- token mgmt --> API1["Internal API A"] BFF -- token mgmt --> API2["Internal API B"] BFF -- SignalR --> WASM["Blazor WASM client"]
BFF keeps tokens on the server; the client never touches access tokens

In code, AddAuthentication().AddOpenIdConnect() combined with AddBff() (from Duende or hand-rolled) is enough. Blazor components use AuthorizeView and [Authorize] just like ordinary ASP.NET Core; in InteractiveWebAssembly the framework automatically calls a /_configuration endpoint to fetch claims from the server cookie.

10. JavaScript Interop — When You Need to Borrow from the JS Garden

Not every library has a .NET equivalent. Chart.js, TinyMCE, video players, OpenLayers, WebRTC… many things still need JS. Blazor lets you call JS from C# and vice versa via IJSRuntime.

@inject IJSRuntime JS

private IJSObjectReference? _module;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _module = await JS.InvokeAsync<IJSObjectReference>(
            "import", "./Components/Chart.razor.js");
        await _module.InvokeVoidAsync("renderChart", "chartCanvas", data);
    }
}

public async ValueTask DisposeAsync()
{
    if (_module is not null)
        await _module.DisposeAsync();
}

Three patterns to remember:

  1. ES module per component — a .razor.js file paired with the component, imported on-demand, with good tree-shaking.
  2. JSObjectReference — hold a reference to a JS object (e.g. a chart instance) and dispose it at the right time to avoid DOM memory leaks.
  3. InvokeAsync with timeout — InteractiveServer may pause due to disconnection; set a TimeSpan so the UI doesn't hang forever.

11. Performance and Observability for Blazor in Production

Whatever the marketing says, Blazor does carry its own "tax": Server mode keeps a circuit in RAM, WASM mode loads the runtime on first visit. A practical production performance checklist:

AreaCommon issue.NET 10 solution
InteractiveServerCircuit occupies 300–500KB RAM per tab, scales poorlyCap CircuitOptions.MaxRetainedDisconnectedCircuits, use a Scale-out SignalR backplane (Azure SignalR Service or a home-grown Redis backplane if you already run Redis)
WASM initial loadLarge bundle, weak devices chokePrerender Static SSR + InteractiveAuto, lazy-load assemblies per route
Grid with 10k+ rowsBlazor re-renders the whole table, lagQuickGrid + Virtualize, change detection via ShouldRender()
Realtime streamingComponent re-renders too often@implements IHandleEvent opts out of default re-render; throttle with System.Threading.Channels
ObservabilityHard to see which renders are slowOpenTelemetry Microsoft.AspNetCore.Components activity source, OTLP export; correlate browser and server traces via traceparent

QuickGrid + Virtualize: example

<QuickGrid Items="orders" Virtualize="true" ItemSize="48">
    <PropertyColumn Property="@(o => o.Id)" />
    <PropertyColumn Property="@(o => o.Customer.Name)" Title="Customer" />
    <PropertyColumn Property="@(o => o.Total)" Format="C0" Sortable="true" />
</QuickGrid>

Virtualize renders only the rows visible in the viewport — fully virtual scrolling. Good for tables up to a few hundred thousand rows on the client (WASM), millions when streamed from the server.

12. Blazor vs Vue 3.6 vs React 19 — When to Pick Which?

This is the most pragmatic question. There's no absolute "better" answer — only a fit-for-context answer.

CriterionBlazor .NET 10Vue 3.6 VaporReact 19 RSC
LanguageC# + RazorTS/JS + SFCTS/JS + JSX
Reuse model/validation with .NET backend★★★★★ — same assembly, DataAnnotations, FluentValidation★★ — via OpenAPI code-gen★★ — via OpenAPI code-gen
Bundle size for a "medium" appStatic SSR 0KB runtime — WASM 1.8–2.4MB~70KB core, Vapor drops VDOM to go even lighter~140KB React + router + query
UI component ecosystemMudBlazor, FluentUI, Radzen, TelerikElement Plus, Vuetify, Naive UI, PrimeVueMUI, Ant, Chakra, shadcn/ui
Hiring marketNarrower, leans enterprise/fintechWide, especially in AsiaWidest globally
Learning curve from .NET backendNear zero — writing C# is writing UIMust learn TS + reactivity + ecosystemMust learn TS + JSX + state + ecosystem
SSR + hydration modelStatic SSR + 4 Render Modes, built-inNuxt 4 separately, island architectureNext.js RSC, frontier but complex
Offline / PWAWASM offline is extremely strongBespoke Service WorkerBespoke Service Worker

A practical formula

If the backend is .NET, the team is under 10 people, and the app is internal/enterprise → Blazor .NET 10. If you need SEO for a large public site and a JS/TS frontend team already exists → Vue/Nuxt or Next.js. Pushing "move everything to Blazor" just to avoid JS is usually a bad call if the team is already strong in React; conversely, internal projects split into two Frontend-Backend teams often cut 30% of headcount once they converge on Blazor.

13. Migration — From Razor Pages, MVC, or Old Blazor Server to Blazor United

One thing that's rarely documented: you don't need to rewrite. Blazor United lives side-by-side with Razor Pages and MVC Controllers in the same process. The path /legacy/invoice can still be classic Razor Pages while /dashboard is Blazor Static SSR + InteractiveAuto.

graph TB
    ENTRY["Program.cs"] --> RP["MapRazorPages"]
    ENTRY --> MVC["MapControllers"]
    ENTRY --> BL["MapRazorComponents<App>().AddInteractiveServerRenderMode().AddInteractiveWebAssemblyRenderMode()"]
    RP --> LEGACY["Legacy pages /invoice, /report (unchanged)"]
    MVC --> API["API /api/*"]
    BL --> BLAZOR["New pages /dashboard, /settings (Blazor)"]
    API -.shared.-> BLAZOR
Blazor United within the same ASP.NET Core host alongside MVC/Razor Pages

A typical roadmap for a mid-sized project:

  1. Weeks 1–2: Upgrade the solution to .NET 10, enable MapRazorComponents<App>(). Keep all existing Razor Pages/MVC.
  2. Weeks 3–6: Pick 1–2 "hot" pages (admin dashboard, dynamic reports) and rewrite them as Blazor components with InteractiveServer. Share DbContext, services, and validation.
  3. Weeks 7–10: Move public pages (landing, product lists) to Blazor Static SSR to leverage Enhanced Navigation + Stream Rendering.
  4. Weeks 11+: Pages with heavy client interaction (editors, canvas, interactive reports) move to InteractiveAuto. Measure real TTI on a mid-tier device from your user base.

Easy-to-miss migration traps

  • Antiforgery token — required by Blazor Enhanced Form; remember app.UseAntiforgery() before the component pipeline.
  • IHttpContextAccessor — Blazor has no concept of HttpContext living through the session; it only exists on first prerender. Anything that depends on HttpContext must be refactored.
  • DbContext lifetime — InteractiveServer uses scope = circuit (lives a long time). Inject IDbContextFactory<T> rather than DbContext directly.
  • Session state — classic ASP.NET Session doesn't work with InteractiveServer the way you'd expect. Use a Scoped service or PersistentComponentState.

14. Production Checklist — Shipping Blazor into a Real System

Server mode checklist

  1. Configure CircuitOptions.DisconnectedCircuitMaxRetained and MaxBufferedUnacknowledgedRenderBatches to match your load model.
  2. Set HubOptions.ClientTimeoutInterval = 30s and KeepAliveInterval = 15s to balance mobile battery vs reconnect.
  3. Enable sticky sessions on the load balancer (or use Azure SignalR Service to avoid stickiness altogether).
  4. Monitor Microsoft.AspNetCore.SignalR metrics via OpenTelemetry: connection duration, message rate, failed reconnect.
  5. Cap per-IP concurrent circuits to mitigate DoS.

WebAssembly mode checklist

  1. Serve .wasm, .webcil, .dat files from a CDN with Brotli + 1-year cache-control + hashed filenames.
  2. Enable SharedArrayBuffer (needs COOP + COEP headers) if using multi-threaded WASM.
  3. Content Security Policy: allow wasm-unsafe-eval, tighten script-src, connect-src.
  4. Enable Response Compression with Brotli at the CDN, fall back to gzip for older browsers.
  5. Measure real Web Vitals in the field (RUM), not just Lighthouse.

Shared — Security & Observability

  1. Always enable app.UseAntiforgery(); verify Enhanced Form isn't disabled by middleware in the wrong order.
  2. Distributed tracing via OpenTelemetry: client (Blazor WASM) → BFF → API → DB. Export OTLP to your existing observability stack (Tempo, Jaeger, Honeycomb…).
  3. End-to-end testing with Playwright — Blazor ships component test helpers since .NET 10 (Microsoft.AspNetCore.Components.Testing) for unit-testing components.
  4. Feature-flag Render Mode per user segment (e.g. enable WASM for 10% of users to measure real performance) — use OpenFeature on both server and client.

15. The Near Future — Blazor After .NET 10

The ASP.NET Core team's public roadmap for 2026–2027 focuses on:

  • Streaming Components in WASM: today stream rendering is strong only on the server; the next phase will bring similar patterns to WASM clients, equivalent to React's Suspense.
  • Partial hydration, island-style: only hydrate components currently visible or being interacted with, reducing JS load on large pages.
  • ML Compile: cooperating with .NET AOT to generate smarter layout code, trimming WASM further.
  • Direct-to-HTTP from WASM: skipping a middle BFF and accessing APIs serverless-style, with tokens granted by WebAuthn — still experimental.

If you ask "Will Blazor replace React/Vue?", the honest answer is still no. JavaScript isn't going away. But Blazor United on .NET 10 is mature enough to become the default choice for .NET teams that want a single-language full-stack, and a significant people-saving option for enterprise apps, admin portals, and internal SaaS. Try writing 2–3 pages in Static SSR + InteractiveAuto next week; you'll find many old prejudices about Blazor no longer apply in 2026.

16. Conclusion

Blazor on .NET 10 is no longer the "interesting idea but not production-ready" story of 2019. The four Render Modes — Static SSR, InteractiveServer, InteractiveWebAssembly, InteractiveAuto — let you pick the right tool for each slice of a page; Stream Rendering and Enhanced Navigation deliver SPA feel while preserving the simple HTTP model. AOT + Trimming + WebCIL bring WASM bundles close to an average JavaScript SPA. Lined up against Vue 3.6 Vapor or React 19, Blazor doesn't win on every axis, but it wins hands-down on sharing model, validation, and logic with a .NET backend — a business case too valuable to ignore for many organisations.

If you're considering a frontend rewrite, don't start by picking a framework. Start by drawing your app's hydration boundaries: which pages need SEO, which need realtime, which can be static, which truly need to run offline. Map those boundaries onto .NET 10's Blazor Render Modes, and the architecture reveals itself — tight, clear, and durable enough to carry you through the LTS window to 2028.

References