Blazor Interatividade em .NET 9 e .NET 10: um guia completo

· 16 min de leitura

Se você tem criado aplicativos da web com o Blazor nos últimos anos, sabe que a estrutura já percorreu um caminho longo. O que começou como uma escolha entre Blazor Server e Blazor WebAssembly evoluiu para um modelo de renderização unificado e flexível que permite escolher a estratégia de interatividade certa para cada componente do seu aplicativo.

Com o .NET 8, obtivemos uma mudança fundamental: a introdução de modos de renderização e renderização estática no lado do servidor (SSR) como padrão. Agora, o .NET 9 e o .NET 10 baseiam-se nessa base com refinamentos que tornam a experiência do desenvolvedor mais suave e a experiência do usuário final mais rápida.

Nesta postagem, quero mostrar o quadro completo da interatividade do Blazor como está hoje: como funcionam os modos de renderização, o que o SSR de streaming traz para a mesa, como a navegação aprimorada e o manuseio de formulários mudam o jogo e o que há de novo nos lançamentos mais recentes. Se você está planejando um novo projeto do Blazor ou pensando em atualizar, este é o guia que eu gostaria de ter quando comecei a me aprofundar em tudo isso.

Uma rápida retrospectiva: como chegamos aqui

Antes do .NET 8, você precisava se comprometer com um modelo de hospedagem no nível do projeto. Blazor Server significava que tudo rodava no servidor por meio de uma conexão SignalR. Blazor WebAssembly significava que tudo rodava no navegador. Cada um tinha vantagens e desvantagens e misturá-las era doloroso.

O .NET 8 mudou o jogo ao introduzir um único modelo de projeto — o Blazor Web App — que unifica os dois modelos. O conceito principal é modos de renderização: você decide por componente como ele deve ser renderizado e onde a interatividade acontece. O padrão tornou-se SSR estático, o que significa que os componentes são renderizados no servidor e enviam HTML simples para o navegador – sem SignalR, sem WebAssembly, apenas HTML rápido.

O .NET 9 aperfeiçoou esses conceitos, melhorou a experiência do desenvolvedor e otimizou o desempenho. O .NET 10 vai além com melhor manipulação de reconexão, estado persistente do componente nos modos de renderização e melhorias na forma como o próprio script Blazor é entregue.

Vamos analisar tudo.

Modos de renderização no .NET 9

No coração do Blazor moderno está o conceito de modos de renderização. Existem quatro modos que você deve conhecer:

1. SSR estático (o padrão)

Quando você cria um novo Blazor Web App, os componentes são renderizados estaticamente no servidor por padrão. O servidor processa o componente Razor, gera HTML e o envia ao navegador. Não há conexão persistente, nem tempo de execução do WebAssembly – apenas solicitação/resposta tradicional.

Isso é perfeito para páginas com muito conteúdo, painéis que exibem principalmente dados ou qualquer página onde você não precise de interação em tempo 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();
    }
}

Esse componente é renderizado no servidor, produz HTML e pronto. Nenhuma conexão contínua. Carregamento inicial rápido, ótimo para SEO, uso mínimo de recursos do servidor.

2. Servidor InterativoQuando você precisar de interatividade em tempo real – manipulação de cliques em botões, processamento de entrada do usuário, atualização dinâmica da UI – você pode optar pelo modo Servidor Interativo. Isso estabelece uma conexão SignalR entre o navegador e o servidor, e as atualizações da IU acontecem por meio dessa conexão.

@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++;
    }
}

A diretiva @rendermode InteractiveServer é suficiente. O componente é renderizado inicialmente no servidor e, em seguida, uma conexão SignalR é estabelecida para lidar com interações subsequentes. Seus manipuladores de eventos C# são executados no servidor e as diferenças da UI são enviadas para o navegador.

Quando usá-lo: Quando você precisa de interatividade, seus componentes precisam de acesso a recursos do lado do servidor (bancos de dados, APIs, sistemas de arquivos) e você deseja um carregamento inicial rápido, sem esperar o download do WebAssembly.

3. WebAssembly interativo

Se você deseja interatividade sem manter uma conexão com o servidor, o Interactive WebAssembly executa a lógica do componente diretamente no navegador usando o tempo de execução do .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();
    }
}

A desvantagem: há um custo inicial de download para o tempo de execução do .NET e seus assemblies. Mas, uma vez carregado, o componente é executado inteiramente no navegador, sem viagens de ida e volta do servidor para interações da interface do usuário.

Quando usar: Para componentes altamente interativos onde a latência é importante (pense: editores de rich text, ferramentas de desenho, filtragem em tempo real), quando você deseja reduzir a carga do servidor ou quando está criando um aplicativo Web progressivo (PWA).

4. Automático Interativo

Este é o meio-termo pragmático e um dos meus recursos favoritos. O Interactive Auto começa com a renderização do lado do servidor via SignalR para o primeiro carregamento e, em seguida, baixa silenciosamente o tempo de execução do WebAssembly em segundo plano. Nas visitas subsequentes, o componente é executado no 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;
    }
}

Isso oferece o melhor dos dois mundos: primeira renderização rápida (sem espera pelo download do WASM) e, eventualmente, o componente é executado no lado do cliente. O usuário não percebe a transição.

Quando usar: Quando você deseja carregamentos iniciais rápidos e eventual execução no lado do cliente. É uma ótima opção padrão para muitos componentes interativos.

Streaming SSR: o melhor dos dois mundos

Streaming SSR é um daqueles recursos que parece simples, mas faz uma enorme diferença no desempenho percebido. A ideia é a seguinte: em vez de esperar que todos os seus dados sejam carregados antes de enviar qualquer HTML, o servidor envia o shell da página imediatamente e então transmite atualizações de conteúdo à medida que os dados ficam disponíveis.

@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();
    }
}

Com o atributo [StreamRendering], eis o que acontece:

  1. O servidor envia imediatamente o HTML com o botão giratório de carregamento.
  2. OnInitializedAsync executa e busca os dados.
  3. Quando os dados chegam, o servidor transmite o HTML atualizado (a tabela) para o navegador.
  4. O navegador corrige o DOM sem recarregar a página inteira.

O usuário vê a página instantaneamente com um indicador de carregamento e, em seguida, o conteúdo é preenchido. Não é necessária nenhuma estrutura JavaScript. Nenhuma conexão WebSocket. Apenas um uso inteligente de streaming HTTP.Quando usar: Qualquer página SSR estática que busca dados durante a renderização. Páginas de listagem de produtos, painéis, relatórios – em qualquer lugar onde o carregamento inicial de dados possa levar mais do que algumas centenas de milissegundos.

Quando NÃO usar: Se os dados forem carregados extremamente rápido (menos de 100 ms), a sobrecarga de streaming não vale a pena. Além disso, o streaming SSR não oferece interatividade contínua – para isso, você ainda precisa de um modo de renderização interativo.

Uma das melhorias sutis, mas poderosas, no Blazor moderno é a navegação aprimorada. Por padrão, o Blazor intercepta cliques em links internos e envios de formulários, buscando o conteúdo da nova página via fetch e corrigindo o DOM em vez de fazer uma navegação completa no navegador.

Isso significa que navegar entre páginas SSR estáticas parece um SPA – sem flash de página inteira, a posição de rolagem pode ser preservada e a experiência é suave como a seda.

Como funciona

Quando o script Blazor (blazor.web.js) é carregado, ele intercepta automaticamente cliques em links internos. Em vez de uma navegação de navegador tradicional, ele:

  1. Faz uma solicitação fetch para o URL de destino.
  2. Recebe a resposta HTML.
  3. Mescla o novo conteúdo no DOM existente.
  4. Atualiza o URL e o histórico do navegador.

Você não precisa fazer nada para ativar isso – ele está ativado por padrão. Mas você pode controlá-lo:

<!-- 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>

Manipulação aprimorada de formulários

Os formulários recebem o mesmo tratamento. Quando você usa EditForm ou o elemento padrão <form> com o tratamento de formulário do Blazor, os envios são interceptados e tratados via 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();
    }
}

O atributo Enhance em EditForm diz ao Blazor para interceptar o envio do formulário. O formulário é enviado para o servidor, o servidor o processa e renderiza novamente a página, e o HTML atualizado é transmitido de volta – tudo sem uma navegação de página inteira. Parece interativo, mas é totalmente renderizado pelo servidor.

Observe o atributo [SupplyParameterFromForm] - é assim que o Blazor vincula os dados do formulário postado ao seu modelo em cenários de SSR estáticos. É a ponte entre as postagens de formulário tradicionais e o modelo de componentes do Blazor.

Interatividade por página e por componente

Um dos aspectos mais poderosos do novo modelo é que você pode misturar modos de renderização em um único aplicativo. Sua página de listagem de produtos pode ser SSR estático, seu carrinho de compras pode ser Interactive Server e seu configurador de produto pode ser Interactive WebAssembly - tudo no mesmo aplicativo.

Configurando modos de renderização no nível do componente

Você pode definir o modo de renderização diretamente em um componente usando a diretiva @rendermode:

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

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

Ou você pode configurá-lo ao usar um componente de um pai:

@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);
    }
}

Neste exemplo, a página em si é SSR estática. O componente ProductConfigurator é executado no WebAssembly para uma rica interatividade do lado do cliente. O componente ProductReviews permanece estático porque está apenas exibindo dados.

Configurando modos de renderização globalmente

Se quiser que todas as páginas sejam interativas por padrão, você pode definir o modo de renderização no nível raiz em App.razor:

<Routes @rendermode="InteractiveServer" />
```Isso torna cada página interativa por meio da renderização do servidor. Componentes individuais ainda podem substituir isso, se necessário.

### Regras importantes a serem lembradas

Existem algumas restrições a serem lembradas ao misturar modos de renderização:

- **Um componente filho não pode ter um modo de renderização "mais interativo" que seu pai.** Se um componente pai for estático, os filhos poderão ser estáticos ou interativos. Mas se um pai for Interactive Server, um filho não poderá ser Interactive WebAssembly (precisaria ser Server ou Auto).
- **Componentes interativos não podem usar diretamente serviços com escopo de pais estáticos.** Se um componente for executado no WebAssembly, ele não poderá acessar instâncias `DbContext` do lado do servidor diretamente  você precisará de uma camada de API.
- **O estado não é transferido automaticamente entre os modos de renderização.** Se um componente for pré-renderizado no servidor e depois alternar para o WebAssembly, você precisará lidar explicitamente com a persistência do estado  mais sobre isso na seção .NET 10.

## O que há de novo no .NET 10

O .NET 10 continua a abordagem de melhoria incremental, com foco na confiabilidade e na experiência do desenvolvedor. Aqui estão os destaques da interatividade do Blazor:

### Experiência de reconexão aprimorada

Se você usou o modo Servidor Interativo na produção, provavelmente encontrou a sobreposição de reconexão  aquele momento em que a conexão do SignalR cai e o usuário  uma mensagem "Reconectando...". No .NET 10, essa experiência fica significativamente melhor.

A lógica de reconexão agora é mais inteligente para tentar novamente. Em vez de um intervalo fixo de novas tentativas, ele usa uma estratégia de espera que se adapta à situação. A IU durante a reconexão também é mais refinada e você tem ganchos melhores para personalizar a experiência:

```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>

A estrutura agora também tenta a reconexão de forma mais agressiva em falhas transitórias e pode restaurar o estado do circuito de forma mais confiável. Isso significa menos momentos do tipo “recarregue a página” para seus usuários.

Estado persistente do componente nos modos de renderização

Este é um grande problema. No .NET 9, quando um componente é pré-renderizado no servidor e depois faz a transição para o WebAssembly (ou alterna entre os modos de renderização), o estado do componente é perdido. O componente é reinicializado efetivamente, o que pode levar a buscas de dados duplicadas e intermitência na interface do usuário.

O .NET 10 melhora o serviço PersistentComponentState para funcionar de maneira mais integrada nas transições do modo de renderização:

@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();
    }
}

Com as melhorias no .NET 10, esse padrão funciona de maneira mais confiável. O estado serializado durante a pré-renderização está adequadamente disponível quando o componente é inicializado em seu modo interativo, evitando buscas duplas de dados e a breve oscilação que os usuários podem ver.

Blazor Script como um ativo da Web estático

Nas versões anteriores, o arquivo Blazor JavaScript (blazor.web.js) era servido a partir do endpoint interno da estrutura. No .NET 10, ele é entregue como um ativo da Web estático. Isso pode parecer uma pequena mudança, mas traz benefícios práticos:- Melhor armazenamento em cache: os ativos estáticos da Web obtêm cabeçalhos de cache e URLs com impressão digital adequados, para que os navegadores os armazenem em cache com mais eficiência.

  • Compatível com CDN: Como é um arquivo estático regular, os CDNs podem armazená-lo em cache e servi-lo a partir de pontos de presença.
  • Compressão: A compactação estática de ativos da Web é aplicada automaticamente, reduzindo o tamanho do script na transmissão.

Você não precisa alterar a forma como faz referência a ele — a estrutura trata o caminho atualizado automaticamente.

Melhores práticas: escolhendo o modo de renderização correto

Depois de trabalhar com todos esses modos em vários projetos, aqui está minha estrutura de decisão:

Comece com SSR estático

Torne o SSR estático seu padrão. A maioria das páginas na maioria dos aplicativos trata principalmente da exibição de dados. Páginas de produtos, postagens de blog, perfis de usuário, páginas de configurações – não precisam de interatividade em tempo real. O SSR estático oferece o melhor desempenho, o menor uso de recursos e o modelo mental mais simples.

Adicione interatividade apenas quando necessário

Identifique os componentes específicos que precisam responder às interações do usuário em tempo real. Um botão “curtir”, um widget de bate-papo, uma interface de arrastar e soltar – tudo isso precisa de interatividade. Mas a página que os cerca provavelmente não.

Use o Interactive Auto como seu modo interativo preferido

Quando você precisa de interatividade, o Interactive Auto costuma ser a melhor opção padrão. Oferece carregamentos iniciais rápidos (renderização do servidor) com eventual execução no lado do cliente (WebAssembly). O usuário obtém o melhor dos dois mundos e você escreve seu código uma vez.

Reservar Servidor Interativo para Casos Específicos

Use o Servidor Interativo quando:

  • Seu componente precisa de acesso direto aos recursos do servidor (bancos de dados, sistema de arquivos, APIs internas).
  • O tamanho do download do WebAssembly é uma preocupação e você não pode usar o Auto.
  • Você precisa que o componente seja sempre executado no servidor por motivos de segurança (por exemplo, processamento de dados confidenciais).

Use streaming SSR generosamente

Se suas páginas SSR estáticas buscarem algum dado, adicione [StreamRendering]. O custo é mínimo e a melhoria de desempenho percebida é significativa. Os usuários veem o conteúdo aparecendo progressivamente, em vez de ficarem olhando para uma página em branco.

Lide com as transições de estado com cuidado

Se você estiver usando Interactive Auto ou misturando pré-renderização com WebAssembly, sempre use PersistentComponentState para evitar buscas de dados duplicadas. Seus usuários agradecerão pela ausência de conteúdo oscilante.

Mantenha a árvore de componentes em mente

Lembre-se das regras de hierarquia do modo de renderização. Planeje sua árvore de componentes para que os limites interativos façam sentido. Um padrão comum é ter um layout estático com “ilhas” interativas incorporadas quando necessário:

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

<main>
    @Body
</main>

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

Conclusão

O modelo de interatividade do Blazor no .NET 9 e .NET 10 representa uma abordagem madura e bem pensada para a construção de aplicativos da web. A capacidade de escolher modos de renderização por componente, a navegação aprimorada e contínua, o SSR de streaming e as melhorias contínuas na reconexão e no gerenciamento de estado tornam-no uma escolha atraente para uma ampla gama de aplicações.A principal ideia é que a interatividade é um espectro, não uma escolha binária. A maior parte do seu aplicativo pode ser estática. Algumas partes precisam de interatividade orientada pelo servidor. Alguns podem se beneficiar da execução no navegador. O Blazor agora permite que você faça essa escolha com a granularidade mais fina — o componente individual — sem lutar contra a estrutura.

Se você estiver iniciando um novo projeto, meu conselho é simples: crie um Blazor Web App, comece com SSR estático, adicione [StreamRendering] a páginas com muitos dados e promova componentes individuais para modos interativos somente quando precisar deles. Você acabará com um aplicativo rápido, eficiente e bem dimensionável.

Boa codificação e, como sempre, fique à vontade para entrar em contato se tiver dúvidas!