Interactividad Blazor en .NET 9 y .NET 10: una guía completa

· 16 min de lectura

Si ha estado creando aplicaciones web con Blazor durante los últimos años, sabrá que el marco ha recorrido un largo camino. Lo que comenzó como una elección entre Blazor Server y Blazor WebAssembly ha evolucionado hasta convertirse en un modelo de representación unificado y flexible que le permite elegir la estrategia de interactividad adecuada para cada componente de su aplicación.

Con .NET 8 obtuvimos el cambio fundamental: la introducción de modos de renderizado y renderizado estático del lado del servidor (SSR) como predeterminados. Ahora, .NET 9 y .NET 10 se basan en esa base con mejoras que hacen que la experiencia del desarrollador sea más fluida y la del usuario final más rápida.

En esta publicación, quiero mostrarle la imagen completa de la interactividad de Blazor tal como está hoy: cómo funcionan los modos de renderizado, qué aporta la transmisión SSR, cómo la navegación mejorada y el manejo de formularios cambian el juego, y qué hay de nuevo en los últimos lanzamientos. Si está planeando un nuevo proyecto Blazor o pensando en actualizarlo, esta es la guía que desearía tener cuando comencé a profundizar en todo esto.

Una mirada retrospectiva: cómo llegamos hasta aquí

Antes de .NET 8, había que comprometerse con un modelo de alojamiento a nivel de proyecto. Blazor Server significaba que todo se ejecutaba en el servidor a través de una conexión SignalR. Blazor WebAssembly significó que todo se ejecutara en el navegador. Cada uno tenía sus compensaciones y mezclarlas fue doloroso.

.NET 8 cambió las reglas del juego al introducir una única plantilla de proyecto (la aplicación web Blazor) que unifica ambos modelos. El concepto clave es modos de renderizado: usted decide por componente cómo debe renderizarse y dónde ocurre la interactividad. El valor predeterminado se convirtió en SSR estático, lo que significa que los componentes se procesan en el servidor y envían HTML simple al navegador: sin SignalR, sin WebAssembly, solo HTML rápido.

.NET 9 pulió estos conceptos, mejoró la experiencia del desarrollador y optimizó el rendimiento. .NET 10 va más allá con un mejor manejo de la reconexión, estado persistente de los componentes en todos los modos de renderizado y mejoras en la forma en que se entrega el script Blazor.

Analicémoslo todo.

Modos de renderizado en .NET 9

En el corazón del Blazor moderno se encuentra el concepto de modos de renderizado. Hay cuatro modos que debes conocer:

1. SSR estático (el valor predeterminado)

Cuando crea una nueva aplicación web Blazor, los componentes se representan estáticamente en el servidor de forma predeterminada. El servidor procesa el componente Razor, genera HTML y lo envía al navegador. No hay conexión persistente ni tiempo de ejecución de WebAssembly, solo solicitud/respuesta tradicional.

Esto es perfecto para páginas con mucho contenido, paneles que muestran principalmente datos o cualquier página donde no necesite interacción en tiempo real.

@page "/products"

<h1>Our Products</h1>

@foreach (var product in products)
{
    <div class="product-card">
        <h3>@product.Name</h3>
        <p>@product.Description</p>
        <span class="price">@product.Price.ToString("C")</span>
    </div>
}

@code {
    private List<Product> products = new();

    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetAllAsync();
    }
}

Este componente se representa en el servidor, produce HTML y eso es todo. No hay conexión continua. Carga inicial rápida, excelente para SEO, uso mínimo de recursos del servidor.

2. Servidor interactivoCuando necesite interactividad en tiempo real (manejar clics en botones, procesar entradas del usuario, actualizar la interfaz de usuario dinámicamente), puede optar por el modo de servidor interactivo. Esto establece una conexión SignalR entre el navegador y el servidor, y las actualizaciones de la interfaz de usuario se realizan a través de esa conexión.

@page "/counter"
@rendermode InteractiveServer

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

La directiva @rendermode InteractiveServer es todo lo que se necesita. El componente se representa inicialmente en el servidor y luego se establece una conexión SignalR para manejar las interacciones posteriores. Sus controladores de eventos de C# se ejecutan en el servidor y las diferencias de la interfaz de usuario se envían al navegador.

Cuándo usarlo: Cuando necesita interactividad, sus componentes necesitan acceso a recursos del lado del servidor (bases de datos, API, sistemas de archivos) y desea una carga inicial rápida sin esperar a que se descargue WebAssembly.

3. Asamblea web interactiva

Si desea interactividad sin mantener una conexión al servidor, Interactive WebAssembly ejecuta la lógica de su componente directamente en el navegador utilizando el tiempo de ejecución de .NET WebAssembly.

@page "/search"
@rendermode InteractiveWebAssembly

<h1>Product Search</h1>

<input @bind="searchTerm" @bind:event="oninput" placeholder="Search products..." />

@if (filteredProducts.Any())
{
    <ul>
        @foreach (var product in filteredProducts)
        {
            <li>@product.Name  @product.Price.ToString("C")</li>
        }
    </ul>
}

@code {
    private string searchTerm = string.Empty;
    private List<Product> allProducts = new();
    private IEnumerable<Product> filteredProducts => allProducts
        .Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));

    protected override async Task OnInitializedAsync()
    {
        allProducts = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? new();
    }
}

La compensación: hay un costo de descarga inicial para el tiempo de ejecución de .NET y sus ensamblados. Pero una vez cargado, el componente se ejecuta completamente en el navegador sin viajes de ida y vuelta al servidor para las interacciones de la interfaz de usuario.

Cuándo usarlo: para componentes altamente interactivos donde la latencia importa (piense: editores de texto enriquecido, herramientas de dibujo, filtrado en tiempo real), cuando desea reducir la carga del servidor o cuando está creando una aplicación web progresiva (PWA).

4. Auto interactivo

Este es el punto medio pragmático y una de mis características favoritas. Interactive Auto comienza con la representación del lado del servidor a través de SignalR para la primera carga y luego descarga silenciosamente el tiempo de ejecución de WebAssembly en segundo plano. En visitas posteriores, el componente se ejecuta en WebAssembly.

@page "/dashboard"
@rendermode InteractiveAuto

<h1>Dashboard</h1>

<DashboardWidget Title="Sales" Value="@salesTotal" />
<DashboardWidget Title="Users" Value="@activeUsers" />

<button @onclick="RefreshData">Refresh</button>

@code {
    private decimal salesTotal;
    private int activeUsers;

    protected override async Task OnInitializedAsync()
    {
        await RefreshData();
    }

    private async Task RefreshData()
    {
        var data = await DashboardService.GetSummaryAsync();
        salesTotal = data.SalesTotal;
        activeUsers = data.ActiveUsers;
    }
}

Esto le brinda lo mejor de ambos mundos: primer renderizado rápido (sin esperar la descarga de WASM) y, finalmente, el componente se ejecuta en el lado del cliente. El usuario no nota la transición.

Cuándo usarlo: Cuando desee cargas iniciales rápidas y una eventual ejecución del lado del cliente. Es una excelente opción predeterminada para muchos componentes interactivos.

Streaming SSR: Lo mejor de ambos mundos

Streaming SSR es una de esas características que suena simple pero marca una gran diferencia en el rendimiento percibido. Esta es la idea: en lugar de esperar a que se carguen todos los datos antes de enviar cualquier HTML, el servidor envía el shell de la página inmediatamente y luego transmite actualizaciones de contenido a medida que los datos están disponibles.

@page "/reports"
@attribute [StreamRendering]

<h1>Monthly Reports</h1>

@if (reports is null)
{
    <div class="loading-spinner">
        <p>Loading reports...</p>
    </div>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Month</th>
                <th>Revenue</th>
                <th>Growth</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var report in reports)
            {
                <tr>
                    <td>@report.Month</td>
                    <td>@report.Revenue.ToString("C")</td>
                    <td class="@(report.Growth >= 0 ? "text-success" : "text-danger")">
                        @report.Growth.ToString("P1")
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private List<MonthlyReport>? reports;

    protected override async Task OnInitializedAsync()
    {
        // This might take a couple of seconds
        reports = await ReportService.GetMonthlyReportsAsync();
    }
}

Con el atributo [StreamRendering], esto es lo que sucede:

  1. El servidor envía inmediatamente el HTML con el control giratorio de carga.
  2. OnInitializedAsync se ejecuta y recupera los datos.
  3. Cuando llegan los datos, el servidor transmite el HTML actualizado (la tabla) al navegador.
  4. El navegador parchea el DOM sin recargar la página completa.

El usuario ve la página instantáneamente con un indicador de carga, luego el contenido se completa. No se necesita un marco de JavaScript. Sin conexión WebSocket. Simplemente uso inteligente de la transmisión HTTP.Cuándo usarlo: Cualquier página SSR estática que obtenga datos durante la renderización. Páginas de listado de productos, paneles de control, informes… en cualquier lugar donde la carga inicial de datos pueda tardar más de unos pocos cientos de milisegundos.

Cuándo NO usarlo: Si los datos se cargan extremadamente rápido (menos de 100 ms), la sobrecarga de transmisión no vale la pena. Además, la transmisión SSR no le brinda interactividad continua; para eso, aún necesita un modo de renderizado interactivo.

Una de las mejoras sutiles pero poderosas del Blazor moderno es la navegación mejorada. De forma predeterminada, Blazor intercepta los clics en enlaces internos y los envíos de formularios, obteniendo el contenido de la nueva página a través de fetch y parcheando el DOM en lugar de realizar una navegación completa en el navegador.

Esto significa que navegar entre páginas SSR estáticas se siente como un SPA: no hay flash de página completa, se puede conservar la posición de desplazamiento y la experiencia es fluida como la seda.

Cómo funciona

Cuando se carga el script Blazor (blazor.web.js), intercepta automáticamente los clics en enlaces internos. En lugar de una navegación de navegador tradicional,:

  1. Realiza una solicitud fetch a la URL de destino.
  2. Recibe la respuesta HTML.
  3. Fusiona el nuevo contenido en el DOM existente.
  4. Actualiza la URL y el historial del navegador.

No tienes que hacer nada para habilitar esto; está activado de forma predeterminada. Pero puedes controlarlo:

<!-- Disable enhanced navigation for a specific link -->
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>

<!-- Force a full page reload for external-like behavior -->
<a href="/downloads/report.pdf" data-enhance-nav="false">Download Report</a>

Manejo de formularios mejorado

Los formularios reciben el mismo tratamiento. Cuando usas EditForm o el elemento estándar <form> con el manejo de formularios de Blazor, los envíos se interceptan y manejan a través de fetch:

@page "/contact"

<EditForm Model="contactModel" OnValidSubmit="HandleSubmit" FormName="contact" Enhance>
    <DataAnnotationsValidator />

    <div class="mb-3">
        <label for="name">Name</label>
        <InputText id="name" @bind-Value="contactModel.Name" class="form-control" />
        <ValidationMessage For="() => contactModel.Name" />
    </div>

    <div class="mb-3">
        <label for="email">Email</label>
        <InputText id="email" @bind-Value="contactModel.Email" class="form-control" />
        <ValidationMessage For="() => contactModel.Email" />
    </div>

    <div class="mb-3">
        <label for="message">Message</label>
        <InputTextArea id="message" @bind-Value="contactModel.Message" class="form-control" />
        <ValidationMessage For="() => contactModel.Message" />
    </div>

    <button type="submit" class="btn btn-primary">Send</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private ContactModel contactModel { get; set; } = new();

    private async Task HandleSubmit()
    {
        await ContactService.SubmitAsync(contactModel);
        contactModel = new ContactModel();
    }
}

El atributo Enhance en EditForm le dice a Blazor que intercepte el envío del formulario. El formulario se publica en el servidor, el servidor lo procesa y vuelve a representar la página, y el HTML actualizado se transmite, todo sin una navegación completa de la página. Parece interactivo, pero está completamente renderizado por el servidor.

Observe el atributo [SupplyParameterFromForm]: así es como Blazor vincula los datos del formulario publicados a su modelo en escenarios de SSR estáticos. Es el puente entre los postes de forma tradicionales y el modelo de componentes de Blazor.

Interactividad por página y por componente

Uno de los aspectos más poderosos del nuevo modelo es que puedes combinar modos de renderizado dentro de una sola aplicación. Su página de listado de productos puede ser SSR estática, su carrito de compras puede ser Interactive Server y su configurador de productos puede ser Interactive WebAssembly, todo en la misma aplicación.

Configuración de modos de renderizado a nivel de componente

Puede configurar el modo de renderizado directamente en un componente usando la directiva @rendermode:

@* This component is interactive via Server *@
@rendermode InteractiveServer

<h3>Live Chat</h3>
<!-- chat UI here -->

O puede configurarlo cuando utilice un componente de un padre:

@page "/product/{Id:int}"

<h1>@product?.Name</h1>
<p>@product?.Description</p>

<!-- This child component gets its own interactive render mode -->
<ProductConfigurator Product="product" @rendermode="InteractiveWebAssembly" />

<!-- This stays static -->
<ProductReviews ProductId="Id" />

@code {
    [Parameter] public int Id { get; set; }
    private Product? product;

    protected override async Task OnInitializedAsync()
    {
        product = await ProductService.GetByIdAsync(Id);
    }
}

En este ejemplo, la página en sí es SSR estática. El componente ProductConfigurator se ejecuta en WebAssembly para una rica interactividad del lado del cliente. El componente ProductReviews permanece estático porque solo muestra datos.

Configuración de modos de renderizado globalmente

Si desea que todas las páginas sean interactivas de forma predeterminada, puede configurar el modo de renderizado en el nivel raíz en App.razor:

<Routes @rendermode="InteractiveServer" />
```Esto hace que cada página sea interactiva a través de la representación del servidor. Los componentes individuales aún pueden anular esto si es necesario.

### Reglas importantes para recordar

Hay algunas limitaciones a tener en cuenta al mezclar modos de renderizado:

- **Un componente hijo no puede tener un modo de renderizado "más interactivo" que su padre.** Si un componente padre es estático, los hijos pueden ser estáticos o interactivos. Pero si un padre es Interactive Server, un hijo no puede ser Interactive WebAssembly (tendría que ser Server o Auto).
- **Los componentes interactivos no pueden usar directamente servicios con alcance de padres estáticos.** Si un componente se ejecuta en WebAssembly, no puede acceder directamente a las instancias `DbContext` del lado del servidor; necesitará una capa API.
- **El estado no se transfiere automáticamente entre modos de renderizado.** Si un componente se prerenderiza en el servidor y luego cambia a WebAssembly, debe manejar la persistencia del estado explícitamente; más sobre esto en la sección .NET 10.

## Novedades de .NET 10

.NET 10 continúa con el enfoque de mejora incremental, centrándose en la confiabilidad y la experiencia del desarrollador. Estos son los aspectos más destacados de la interactividad de Blazor:

### Experiencia de reconexión mejorada

Si ha utilizado el modo de servidor interactivo en producción, probablemente haya encontrado la superposición de reconexión: ese momento en el que se interrumpe la conexión de SignalR y el usuario ve el mensaje "Reconectando...". En .NET 10, esta experiencia mejora significativamente.

La lógica de reconexión ahora es más inteligente en cuanto a reintentar. En lugar de un intervalo de reintento fijo, utiliza una estrategia de retroceso que se adapta a la situación. La interfaz de usuario durante la reconexión también está más pulida y tienes mejores opciones para personalizar la experiencia:

```razor
<!-- In your App.razor or layout -->
<div id="components-reconnect-modal">
    <div class="reconnect-visible">
        <p>Connection lost. Attempting to reconnect...</p>
        <div class="spinner"></div>
    </div>
    <div class="reconnect-failed">
        <p>Could not reconnect to the server.</p>
        <button onclick="location.reload()">Reload</button>
    </div>
    <div class="reconnect-rejected">
        <p>Your session has expired.</p>
        <a href="/">Return to Home</a>
    </div>
</div>

El marco ahora también intenta la reconexión de manera más agresiva en caso de fallas transitorias y puede restaurar el estado del circuito de manera más confiable. Esto significa menos momentos de “recargar la página” para sus usuarios.

Estado de componente persistente en todos los modos de renderizado

Este es uno grande. En .NET 9, cuando un componente se renderiza previamente en el servidor y luego pasa a WebAssembly (o cambia entre modos de renderizado), el estado del componente se pierde. El componente se reinicia de manera efectiva, lo que puede generar búsquedas de datos duplicadas y una interfaz de usuario parpadeante.

.NET 10 mejora el servicio PersistentComponentState para que funcione de manera más fluida en las transiciones del modo de renderizado:

@page "/weather"
@rendermode InteractiveWebAssembly
@inject PersistentComponentState ApplicationState

<h1>Weather Forecast</h1>

@if (forecasts is null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var forecast in forecasts)
    {
        <div class="forecast-card">
            <h4>@forecast.Date.ToShortDateString()</h4>
            <p>@forecast.Summary  @forecast.TemperatureC°C</p>
        </div>
    }
}

@code {
    private List<WeatherForecast>? forecasts;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);

        if (!ApplicationState.TryTakeFromJson<List<WeatherForecast>>(
            "weather-forecasts", out var restored))
        {
            // Data wasn't persisted from prerendering — fetch it
            forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("api/weather");
        }
        else
        {
            forecasts = restored;
        }
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("weather-forecasts", forecasts);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        persistingSubscription.Dispose();
    }
}

Con las mejoras en .NET 10, este patrón funciona de manera más confiable. El estado serializado durante la renderización previa está disponible correctamente cuando el componente se inicializa en su modo interactivo, lo que evita la doble obtención de datos y el breve parpadeo que los usuarios pueden ver.

Blazor Script como activo web estático

En versiones anteriores, el archivo JavaScript de Blazor (blazor.web.js) se entregaba desde el punto final interno del marco. En .NET 10, se entrega como un recurso web estático. Esto puede parecer un pequeño cambio, pero tiene beneficios prácticos:- Mejor almacenamiento en caché: Los recursos web estáticos obtienen encabezados de caché adecuados y URL con huellas digitales, por lo que los navegadores los almacenan en caché de manera más efectiva.

  • Compatible con CDN: Dado que es un archivo estático normal, las CDN pueden almacenarlo en caché y servirlo desde ubicaciones perimetrales.
  • Compresión: La compresión estática de recursos web se aplica automáticamente, lo que reduce el tamaño del script en la conexión.

No es necesario cambiar la forma en que se hace referencia a él: el marco maneja la ruta actualizada automáticamente.

Mejores prácticas: elegir el modo de renderizado correcto

Después de trabajar con todos estos modos en varios proyectos, aquí está mi marco de decisión:

Comience con SSR estático

Haga que SSR estático sea su valor predeterminado. La mayoría de las páginas en la mayoría de las aplicaciones tratan principalmente de mostrar datos. Páginas de productos, publicaciones de blogs, perfiles de usuario, páginas de configuración: no necesitan interactividad en tiempo real. Static SSR le brinda el mejor rendimiento, el menor uso de recursos y el modelo mental más simple.

Agregue interactividad solo donde sea necesario

Identifique los componentes específicos que deben responder a las interacciones del usuario en tiempo real. Un botón “Me gusta”, un widget de chat, una interfaz de arrastrar y soltar: todo esto necesita interactividad. Pero la página que los rodea probablemente no sea así.

Utilice el modo interactivo automático como modo interactivo de acceso

Cuando necesita interactividad, Interactive Auto suele ser la mejor opción predeterminada. Le brinda cargas iniciales rápidas (renderizado del servidor) con eventual ejecución del lado del cliente (WebAssembly). El usuario obtiene lo mejor de ambos mundos y usted escribe su código una vez.

Reservar servidor interactivo para casos específicos

Utilice Interactive Server cuando:

  • Su componente necesita acceso directo a los recursos del servidor (bases de datos, sistema de archivos, API internas).
  • El tamaño de descarga de WebAssembly es un problema y no se puede utilizar Auto.
  • Necesita que el componente se ejecute siempre en el servidor por razones de seguridad (por ejemplo, procesamiento de datos confidenciales).

Utilice Streaming SSR generosamente

Si sus páginas SSR estáticas obtienen algún dato, agregue [StreamRendering]. El coste es mínimo y la mejora percibida en el rendimiento es significativa. Los usuarios ven que el contenido aparece progresivamente en lugar de mirar una página en blanco.

Maneje las transiciones de estado con cuidado

Si utiliza Interactive Auto o combina el renderizado previo con WebAssembly, utilice siempre PersistentComponentState para evitar recuperaciones de datos duplicadas. Tus usuarios te agradecerán la ausencia de contenido parpadeante.

Tenga en cuenta el árbol de componentes

Recuerde las reglas de jerarquía del modo de renderizado. Planifique su árbol de componentes para que los límites interactivos tengan sentido. Un patrón común es tener un diseño estático con “islas” interactivas integradas donde sea necesario:

@* Layout: Static SSR *@
<header>
    <NavMenu />
    <UserMenu @rendermode="InteractiveServer" /> @* Needs real-time auth state *@
</header>

<main>
    @Body
</main>

<footer>
    <ChatWidget @rendermode="InteractiveAuto" /> @* Rich interactivity *@
</footer>

Conclusión

El modelo de interactividad de Blazor en .NET 9 y .NET 10 representa un enfoque maduro y bien pensado para crear aplicaciones web. La capacidad de elegir modos de renderizado por componente, la navegación mejorada y fluida, la transmisión SSR y las mejoras continuas en la reconexión y la gestión del estado lo convierten en una opción convincente para una amplia gama de aplicaciones.La idea clave es que la interactividad es un espectro, no una elección binaria. La mayor parte de su aplicación puede ser estática. Algunas partes necesitan interactividad impulsada por el servidor. Algunos podrían beneficiarse si se ejecutan en el navegador. Blazor ahora le permite tomar esa decisión con la granularidad más fina (el componente individual) sin tener que luchar contra el marco.

Si está iniciando un nuevo proyecto, mi consejo es simple: cree una aplicación web Blazor, comience con SSR estático, agregue [StreamRendering] a páginas con muchos datos y promueva componentes individuales a modos interactivos solo cuando los necesite. Terminará con una aplicación que es rápida, eficiente y escalable bien.

Feliz codificación y, como siempre, no dudes en comunicarte con nosotros si tienes preguntas.