Blazor: modos de renderização, WebAssembly e estado

Blazor: modos de renderização, WebAssembly e estado

Os quatro modos de renderização do Blazor e como escolher por componente: SSR estático, Server com circuito, WebAssembly no navegador e Auto. O carrinho vai para o WebAssembly, uma interface com duas implementações, [PersistentState] contra a carga dupla do pré-render e estado compartilhado entre componentes.
Linguagem C#

• • 22 min de leitura

Um componente Blazor pode rodar em três lugares: montado uma vez no servidor, vivo no servidor com um circuito, ou dentro do navegador em WebAssembly. Neste artigo vemos o que muda em cada caso, como escolher por componente e como o estado atravessa essas fronteiras.

Onde o código roda

No artigo anterior o carrinho rodava no servidor, e o navegador só exibia o resultado. Isso tem consequências que ainda não examinamos:

  • Cada aba com o carrinho aberto mantém um circuito: uma conexão SignalR permanente e toda a árvore de componentes na memória do servidor, com os dados que ela guarda. Dezenas de kilobytes a alguns megabytes por usuário, dependendo do componente. Com dez mil visitantes simultâneos, isso é uma quantidade grande de RAM.
  • Cada clique é uma ida e volta pela rede. Com 20 ms de latência ninguém percebe. Com 300 ms, num 4G ruim, o "+" do seletor parece travado.
  • Uma queda da conexão interrompe a interface. O script mostra o aviso de reconexão, e se o servidor reiniciar nesse meio-tempo, o estado do circuito se perde.

A alternativa é rodar o componente no navegador. O WebAssembly (Wasm) é um formato binário que todos os navegadores modernos executam numa sandbox, com velocidade próxima da nativa. O Blazor WebAssembly envia ao navegador um runtime .NET compilado para Wasm e as DLLs da aplicação, e o componente roda lá, sem circuito e sem ida ao servidor a cada clique. As contrapartidas são outras:

  • Download inicial: o runtime e as bibliotecas, alguns megabytes comprimidos, com trimming removendo o código não usado. Depois disso ficam no cache do navegador.
  • Tudo o que vai para o navegador é público. As DLLs podem ser baixadas e descompiladas. Nenhum segredo, nenhuma connection string, nenhuma regra que o usuário não deva ver ou contornar.
  • Nenhum acesso direto ao servidor: nem banco, nem Redis, nem arquivos. Só HTTP, como qualquer código de navegador.

Nenhum dos dois é melhor sempre. No .NET 8 o Blazor passou a permitir escolher por componente.

Os quatro modos

O modelo unificado (Blazor Web App) oferece quatro modos de renderização:

Modo Onde roda Interativo? Custo principal
SSR estático servidor, uma vez por requisição não (formulários com POST) nenhum além da requisição
Interactive Server servidor, com circuito SignalR sim memória e conexão por usuário; latência por clique
Interactive WebAssembly navegador sim download inicial; código público
Interactive Auto servidor no primeiro acesso, navegador quando o Wasm já está em cache sim a soma das complexidades dos dois

O SSR estático é, na prática, o que as Razor Pages já fazem, só que escrito como componente: o servidor renderiza o HTML e esquece o componente. Não há @onclick funcionando, mas formulários funcionam com POST, como em "Formulários e validação". A maior parte de um site deveria ficar nesse modo. A interatividade é acrescentada só onde ela se justifica, as chamadas "ilhas de interatividade".

A escolha é declarada no componente ou no ponto em que ele é usado:

@page "/carrinho"
@rendermode InteractiveWebAssembly
<ResumoDoPedido @rendermode="InteractiveServer" Subtotal="..." />

Há uma regra de fronteira: um componente dentro de uma região interativa herda o modo dela, e não é possível misturar modos interativos diferentes dentro da mesma subárvore. Uma ilha Server não contém uma ilha WebAssembly. Os parâmetros que atravessam a fronteira de um componente estático para um interativo são serializados em JSON e embutidos na página. Por isso só podem ser tipos serializáveis: nada de RenderFragment, delegates ou serviços.

Migrando para o modelo unificado

O modelo clássico do artigo anterior (AddServerSideBlazor, blazor.server.js, o tag helper <component>) só oferece Server e WebAssembly em modo legado. Para ter os quatro modos, o site passa a hospedar um Blazor Web App ao lado das Razor Pages. A solução ganha um projeto para o código que vai rodar no navegador:

dotnet new blazorwasm -n Livraria.Site.Client -o src/Livraria.Site.Client --empty
dotnet add src/Livraria.Site reference src/Livraria.Site.Client
dotnet add src/Livraria.Site package Microsoft.AspNetCore.Components.WebAssembly.Server

O Livraria.Site.Client usa o SDK Microsoft.NET.Sdk.BlazorWebAssembly e contém tudo o que pode rodar no navegador: os componentes do carrinho, os records LinhaVisivel e EntradaDeFrete, e as interfaces ICarrinhos e ICotadorDeFrete. Ele não pode referenciar nada que dependa do servidor, e o compilador impede, porque o SDK do WebAssembly não tem acesso a Redis, HttpContext ou IDistributedCache. As implementações de servidor continuam no Livraria.Site.

O template blazorwasm cria um aplicativo independente, e não o projeto cliente de um Blazor Web App, então pede uma limpeza: apague wwwroot/index.html, App.razor, a pasta Layout e as páginas de exemplo, tire do Program.cs as linhas RootComponents.Add e acrescente ao _Imports.razor a linha @using static Microsoft.AspNetCore.Components.Web.RenderMode. Sem ela, @rendermode InteractiveWebAssembly não compila (CS0103); sem a limpeza, o App do cliente colide com o do servidor (CS0104). Medido no .NET 10.

No servidor:

builder.Services.AddRazorPages();
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

// ...
app.UseAntiforgery();
app.MapRazorPages();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Livraria.Site.Client._Imports).Assembly);

O App.razor é a raiz do documento HTML para as rotas de componentes, com <HeadOutlet />, <Routes /> e o script unificado <script src="_framework/blazor.web.js"></script>, que substitui o blazor.server.js. As Razor Pages continuam com o layout delas, e as páginas de componentes usam um MainLayout.razor. Para não duplicar cabeçalho e rodapé, eles viram componentes, CabecalhoDaLoja.razor e RodapeDaLoja.razor. O MainLayout os usa diretamente, e o _Layout.cshtml das Razor Pages os renderiza com <component type="typeof(CabecalhoDaLoja)" render-mode="Static" />. Uma única fonte de marcação serve os dois mundos durante a transição.

/carrinho deixa de ser uma Razor Page e passa a ser um componente com rota, no projeto cliente:

@* Livraria.Site.Client/Paginas/PaginaDoCarrinho.razor *@
@page "/carrinho"
@rendermode InteractiveWebAssembly

<PageTitle>Seu carrinho · Livraria</PageTitle>
<h1>Seu carrinho</h1>
<Carrinho />

Qual modo para o carrinho

A decisão mais instrutiva do artigo é de onde vem o estado do componente, mais do que a velocidade.

O carrinho é identificado pelo cookie carrinho. No servidor, durante uma requisição HTTP, o cookie está disponível. No Interactive Server, porém, depois do pré-render o componente vive num circuito SignalR que não é uma requisição HTTP: HttpContext não está disponível de forma confiável, e IHttpContextAccessor pode devolver nulo ou um contexto que não corresponde ao usuário atual. É um erro clássico de quem migra do MVC para o Blazor Server. No WebAssembly não existe HttpContext nenhum. Mas o navegador envia o cookie automaticamente em toda chamada HTTP para a mesma origem.

A escolha fica assim:

  • Interactive Server exigiria capturar o id do carrinho durante o pré-render e levá-lo para dentro do circuito. É possível, com a técnica da próxima seção, mas é uma peça a mais.
  • Interactive WebAssembly com pré-render fica mais natural. No pré-render, o componente roda no servidor, dentro da requisição, e a implementação de servidor lê o cookie. No navegador, a implementação cliente chama endpoints do próprio site, e o cookie vai junto sem ninguém pensar nele.
  • Interactive Auto herdaria a dificuldade do Server na primeira visita.

Para o carrinho escolhemos WebAssembly. O preço é que, no primeiro acesso de um visitante, os botões só respondem depois que o runtime é baixado, normalmente um ou dois segundos numa conexão razoável. Até lá o HTML pré-renderizado já está visível. Nas visitas seguintes, tudo está em cache e a interatividade é praticamente imediata.

Duas implementações, uma interface

O componente depende só da interface. Cada ambiente registra a sua implementação:

// Livraria.Site.Client — contrato e implementação do navegador
public interface ICarrinhos
{
    Task<IReadOnlyList<LinhaVisivel>> ObterAsync(CancellationToken ct);
    Task<IReadOnlyList<LinhaVisivel>> AlterarAsync(int produtoId, int quantidade, CancellationToken ct);
    Task<IReadOnlyList<LinhaVisivel>> RemoverAsync(int produtoId, CancellationToken ct);
}

public sealed class CarrinhosViaBff(HttpClient http) : ICarrinhos
{
    public async Task<IReadOnlyList<LinhaVisivel>> ObterAsync(CancellationToken ct) =>
        (await http.GetFromJsonAsync<List<LinhaVisivel>>("bff/carrinho", ct))!;

    public async Task<IReadOnlyList<LinhaVisivel>> AlterarAsync(int produtoId, int quantidade, CancellationToken ct)
    {
        using var resposta = await http.PutAsJsonAsync($"bff/carrinho/{produtoId}", new { quantidade }, ct);
        resposta.EnsureSuccessStatusCode();
        return (await resposta.Content.ReadFromJsonAsync<List<LinhaVisivel>>(ct))!;
    }

    public async Task<IReadOnlyList<LinhaVisivel>> RemoverAsync(int produtoId, CancellationToken ct)
    {
        using var resposta = await http.DeleteAsync($"bff/carrinho/{produtoId}", ct);
        resposta.EnsureSuccessStatusCode();
        return (await resposta.Content.ReadFromJsonAsync<List<LinhaVisivel>>(ct))!;
    }
}
// Livraria.Site.Client/Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<ICarrinhos, CarrinhosViaBff>();
builder.Services.AddScoped<ICotadorDeFrete, CotadorViaBff>();
await builder.Build().RunAsync();

A interface mudou em relação ao artigo anterior. Ela agora trabalha com linhas prontas para exibir e não recebe mais o id do carrinho. O componente não sabe que existe cookie, Redis ou API. Pede "o meu carrinho", e cada implementação resolve à sua maneira.

No servidor, a implementação compõe o armazenamento do artigo anterior com o catálogo, e os mesmos serviços atendem os endpoints que o navegador chama:

// Livraria.Site — implementação de servidor
public sealed class CarrinhosNoServidor(
    IHttpContextAccessor acessor, IArmazemDeCarrinhos armazem, ICatalogoDaLoja catalogo) : ICarrinhos
{
    private string Id =>
        acessor.HttpContext?.Request.Cookies["carrinho"]
        ?? throw new InvalidOperationException("Carrinho acessado fora de uma requisição HTTP.");

    public async Task<IReadOnlyList<LinhaVisivel>> ObterAsync(CancellationToken ct) =>
        await MontarLinhasAsync(await armazem.ObterAsync(Id, ct), ct);

    // AlterarAsync e RemoverAsync: leem, alteram, salvam e devolvem as linhas montadas
}

// Program.cs do servidor
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICarrinhos, CarrinhosNoServidor>();

var bff = app.MapGroup("/bff").RequireRateLimiting("Site");
bff.MapGet("/carrinho", (ICarrinhos c, CancellationToken ct) => c.ObterAsync(ct));
bff.MapPut("/carrinho/{produtoId:int}", (int produtoId, AlteracaoDeQuantidade a, ICarrinhos c, CancellationToken ct) =>
    c.AlterarAsync(produtoId, a.Quantidade, ct));
bff.MapDelete("/carrinho/{produtoId:int}", (int produtoId, ICarrinhos c, CancellationToken ct) =>
    c.RemoverAsync(produtoId, ct));

O IArmazemDeCarrinhos é o antigo CarrinhosDistribuidos, renomeado. Aqui o IHttpContextAccessor é seguro, porque essa implementação só é usada em requisições HTTP de verdade: no pré-render e nos endpoints /bff. A exceção explícita documenta essa suposição e falha alto se alguém um dia registrar o componente em modo Server.

Esses endpoints /bff são o embrião do padrão Backend for Frontend mencionado em "Autenticação e autorização com JWT": o navegador conversa só com o próprio site, que guarda os segredos e fala com os serviços internos. O próximo artigo generaliza a ideia.

O pré-render e o estado que atravessa

O pré-render tem um custo que aparece assim que se mede. O OnInitializedAsync do carrinho roda duas vezes: uma no servidor, para gerar o HTML, e outra no navegador, quando o componente se torna interativo. O componente no navegador é uma instância nova, sem memória do que o servidor calculou. São duas leituras do carrinho, duas idas ao catálogo e, pior, um piscar na tela: o HTML pré-renderizado mostra os livros, o componente interativo começa com linhas nulo e mostra "Carregando…", e depois mostra os livros de novo.

A solução é persistir o estado do pré-render na página e reaproveitá-lo no navegador. No .NET 10 isso ficou declarativo:

@code {
    [PersistentState]
    public List<LinhaVisivel>? Linhas { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Linhas ??= (await Carrinhos.ObterAsync(cancelamento.Token)).ToList();
    }
}

No pré-render, Linhas começa nula, é carregada, e ao fim da renderização o Blazor serializa o valor em JSON num comentário da página. No navegador, antes de OnInitializedAsync, o valor é restaurado, o ??= não faz nada, e a interface interativa nasce idêntica à pré-renderizada, sem segunda leitura e sem piscar. A propriedade precisa ser pública e serializável. Em versões anteriores ao .NET 10, o mesmo efeito se obtém de forma imperativa, com o serviço PersistentComponentState: registra-se um callback com RegisterOnPersisting para gravar e usa-se TryTakeFromJson para ler.

Todo estado persistido vai para o HTML e é visível para quem abrir o código-fonte da página. Para o carrinho do próprio usuário não há problema. Para qualquer coisa sensível, há.

Estado compartilhado entre componentes

O cabeçalho mostra um contador de itens do carrinho, e ele precisa mudar quando o carrinho muda, embora os dois componentes não sejam pai e filho. O padrão é um contêiner de estado: um serviço com os dados e um evento de mudança.

public sealed class EstadoDoCarrinho
{
    public int QuantidadeDeItens { get; private set; }
    public event Action? Mudou;

    public void Atualizar(IReadOnlyList<LinhaVisivel> linhas)
    {
        QuantidadeDeItens = linhas.Sum(l => l.Quantidade);
        Mudou?.Invoke();
    }
}
@* MiniCarrinho.razor *@
@implements IDisposable
@inject EstadoDoCarrinho Estado

<a href="/carrinho" class="mini-carrinho">
    Carrinho <span class="contador" aria-label="@Estado.QuantidadeDeItens itens">@Estado.QuantidadeDeItens</span>
</a>

@code {
    protected override void OnInitialized() => Estado.Mudou += AoMudar;

    private void AoMudar() => InvokeAsync(StateHasChanged);

    public void Dispose() => Estado.Mudou -= AoMudar;
}

Três detalhes merecem atenção.

StateHasChanged pede uma nova renderização. O Blazor já o chama sozinho depois de eventos da própria interface e de EventCallback. Um evento vindo de um serviço é outra coisa, e o componente precisa pedir a renderização.

InvokeAsync garante que a renderização aconteça no contexto do componente. No Server, o evento pode ser disparado de outra thread, e mexer no componente fora do contexto do circuito lança exceção. No WebAssembly há uma única thread, mas o hábito mantém o componente portável entre os modos.

Cancelar a assinatura no Dispose evita um vazamento de memória. Um componente descartado que continua inscrito no evento de um serviço de vida mais longa não é coletado, e continua tentando renderizar. É o mesmo problema de eventos visto em "Delegates, lambdas e eventos", agora com consequências visíveis.

O tempo de vida de um serviço scoped depende do modo, e isso surpreende quem vem do ASP.NET:

Modo "Scoped" significa
SSR estático uma requisição HTTP
Interactive Server um circuito, ou seja, uma aba do navegador enquanto a conexão durar
Interactive WebAssembly a aplicação inteira no navegador (na prática, um singleton por aba)

Um EstadoDoCarrinho registrado como scoped é compartilhado por todos os componentes interativos da mesma aba e do mesmo modo. Componentes Server e WebAssembly na mesma página não compartilham o serviço, porque vivem em processos diferentes. É mais um motivo para agrupar componentes que conversam entre si no mesmo modo.

Tempo real no navegador

Em WebAssembly, o carrinho pode assinar o hub da Livraria diretamente, com o cliente C# de "Tempo real com SignalR", que funciona dentro do Wasm:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!firstRender || !OperatingSystem.IsBrowser())
        return;

    conexao = new HubConnectionBuilder()
        .WithUrl(Navegacao.ToAbsoluteUri("/api/hubs/livraria"))
        .WithAutomaticReconnect()
        .Build();

    conexao.On<int>("ProdutoAlterado", async id =>
    {
        if (Linhas?.Any(l => l.ProdutoId == id) == true)
        {
            Linhas = (await Carrinhos.ObterAsync(cancelamento.Token)).ToList();
            await InvokeAsync(StateHasChanged);
        }
    });

    await conexao.StartAsync();
    foreach (var linha in Linhas ?? [])
        await conexao.InvokeAsync("AcompanharProduto", linha.ProdutoId);
}

A conexão é aberta em OnAfterRenderAsync(firstRender), e não em OnInitializedAsync, porque OnInitializedAsync também roda no pré-render, no servidor, onde abrir uma conexão com o hub não serve para nada e só atrasa a resposta. OnAfterRender nunca roda no pré-render. O OperatingSystem.IsBrowser() é uma segunda garantia de que o código só roda no WebAssembly. O IAsyncDisposable do componente fecha a conexão. O endereço /api/hubs/livraria passa pelo próprio site, e é o próximo artigo que faz esse caminho existir.

Se esse mesmo componente rodasse em Interactive Server, cada circuito abriria sua própria conexão do servidor do site com a API, e dez mil carrinhos abertos seriam dez mil conexões SignalR entre dois servidores. No modo Server, o desenho certo é uma conexão singleton do site com a API, que redistribui as mensagens por um evento de serviço para os circuitos interessados. O modo de renderização muda a arquitetura, não só o lugar em que o código roda.

Quando algo dá errado

Uma exceção não tratada num componente interativo encerra o circuito, no Server, ou a aplicação, no WebAssembly, e mostra a barra de erro padrão. O componente ErrorBoundary contém o estrago numa região:

<ErrorBoundary>
    <ChildContent>
        <ResumoDoPedido Subtotal="Subtotal" QuantidadeDeItens="Quantidade" />
    </ChildContent>
    <ErrorContent>
        <p role="alert">Não conseguimos exibir o resumo agora. Seu carrinho está salvo.</p>
    </ErrorContent>
</ErrorBoundary>

É o equivalente de interface do try/catch do view component de mais vendidos em "Razor Pages": uma parte secundária que falha não derruba a página.

No Interactive Server, quedas de conexão são rotina (o celular troca de rede, o notebook hiberna). O script tenta reconectar ao mesmo circuito. Se o servidor já o tiver descartado, a interface pede para recarregar. O .NET 10 acrescentou a possibilidade de persistir o estado do circuito quando ele é desconectado e restaurá-lo num circuito novo, o que torna esse cenário menos frequente. Ainda assim, componentes Server devem guardar o que importa num armazenamento durável, como fizemos com o carrinho, e não confiar na memória do circuito.

A escolha, componente por componente

Parte da Livraria Modo Por quê
Vitrine, página do livro, busca SSR estático (Razor Pages) conteúdo público, indexável, rápido no primeiro acesso
Disponibilidade ao vivo na página do livro SSR + JavaScript pequeno aprimoramento progressivo já resolvido em "Razor Pages"
Carrinho Interactive WebAssembly interação intensa, estado do próprio usuário via cookie, sem custo por usuário no servidor
Contador do cabeçalho Interactive WebAssembly precisa conversar com o carrinho, então fica no mesmo modo
Área administrativa Interactive Server poucos usuários, acesso direto a serviços internos, código que não deve ir para o navegador

A última linha antecipa um artigo adiante: a área administrativa tem outros requisitos, e o modo Server se encaixa melhor nela.

Agora a Livraria sabe onde cada parte deve rodar e por quê: páginas públicas estáticas e indexáveis, um carrinho que roda no navegador sem custo por visitante, estado que atravessa o pré-render sem ser buscado duas vezes, e serviços com uma interface e duas implementações, uma para cada lado da fronteira. Os endpoints /bff resolveram o carrinho, mas outras chamadas continuam espalhadas: o catálogo, o frete e o hub, cada uma com sua URL, seu tratamento de erro e nenhuma resiliência.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um desenvolvedor colocou no projeto cliente uma classe CalculadoraDeDesconto com a regra "cupom LIVRO10 dá 10% em pedidos acima de R$ 100", e o carrinho aplica o desconto no total exibido e no pedido enviado. Liste os problemas.

Ver resposta

✓ Resposta: (1) A regra é pública: as DLLs do WebAssembly podem ser baixadas e descompiladas, então qualquer um descobre os cupons existentes e suas condições. (2) A regra pode ser contornada: se o valor com desconto ou o próprio cálculo for enviado pelo cliente, basta alterar a requisição ou o código no navegador para aplicar qualquer desconto. (3) Duplicação: a regra precisaria existir também na API para ser verificada, e as duas cópias vão divergir. A correção é que o cliente envia só o código do cupom, a API valida e calcula o desconto a partir dos seus próprios dados, e devolve o total. O componente pode mostrar uma prévia chamando um endpoint de cotação, mas nunca é a fonte da verdade. Regra que envolve dinheiro ou permissão vive no servidor.

Exercício 2

Explique por que a implementação CarrinhosNoServidor, que usa IHttpContextAccessor, funciona no pré-render do carrinho em WebAssembly, mas quebraria se alguém mudasse a página para @rendermode InteractiveServer.

Ver resposta

✓ Resposta: No pré-render, o componente roda no servidor durante a requisição HTTP de GET /carrinho: o HttpContext é real e o cookie está lá. No modo WebAssembly, depois do pré-render, o componente roda no navegador com CarrinhosViaBff, e cada chamada /bff é outra requisição HTTP real, em que a implementação de servidor volta a ter contexto. No Interactive Server, depois do pré-render o componente continua no servidor, mas dentro de um circuito SignalR. Os eventos (clicar em "+") não são requisições HTTP. O IHttpContextAccessor pode devolver nulo, e a exceção explícita dispara, ou, em cenários com cache de contexto, devolver o contexto da conexão inicial, que não é garantido nem atualizado. A correção nesse modo seria capturar o id do carrinho durante o pré-render e persisti-lo com [PersistentState], ou passá-lo como parâmetro serializado. A exceção explícita no código transforma esse erro sutil em uma falha clara.

Exercício 3

Um colega registrou EstadoDoCarrinho como singleton no servidor "porque scoped não funcionava no modo Server". O que acontece em produção com vários visitantes?

Ver resposta

✓ Resposta: Um singleton no servidor é único para todos os usuários. No modo Server, todos os circuitos compartilhariam o mesmo EstadoDoCarrinho: quando Ana adiciona um livro, o contador do cabeçalho de Bruno também muda, e as quantidades se misturam. Além disso, o evento Mudou acumularia assinaturas de componentes de todos os usuários, disparando renderizações em circuitos alheios, e qualquer circuito que esquecesse de cancelar a assinatura vazaria memória para sempre. O "não funcionava" provavelmente era outra coisa, como componentes em modos diferentes que não compartilham serviços, ou falta de InvokeAsync(StateHasChanged). No Server, scoped já significa "por circuito", que é exatamente o escopo de um usuário numa aba. Estado de usuário nunca vai em singleton no servidor.

Exercício 4

Com [PersistentState], o HTML do carrinho pré-renderizado carrega a lista de linhas serializada. Estime o impacto para um carrinho de 5 livros, e diga que tipo de dado não deveria ser persistido desse jeito, mesmo sendo do próprio usuário.

Ver resposta

✓ Resposta: Cada LinhaVisivel tem id, título, URL, preço, quantidade e estoque, e o Subtotal calculado também é serializado: cerca de 140 bytes em JSON para um título curto (medido no .NET 10). Cinco linhas dão uns 700 bytes, que o Blazor codifica em Base64 duas vezes (o valor e o dicionário que guarda todos os valores persistidos), e chegam a cerca de 1,3 KB na página, antes da compressão HTTP. É irrelevante perto de uma imagem de capa, e bem mais barato que uma segunda ida ao catálogo. O que não deve ser persistido: (a) segredos e tokens, como um access token da API, que acabariam no HTML, em caches de página e no histórico; (b) dados de outros usuários ou internos, como custo de aquisição ou margem, que o componente usou no servidor mas o cliente não deve ver; (c) volumes grandes, como listas de centenas de itens, que incham a página e pioram o tempo de primeira exibição. Nesse caso é melhor buscar no cliente, com paginação.

Exercício 5

Sua equipe discute usar Interactive Auto para o carrinho, argumentando que "a primeira visita fica interativa na hora". Construa o argumento contra, considerando o que o artigo mostrou sobre estado.

Ver resposta

✓ Resposta: O Auto roda o componente em modo Server até o Wasm estar baixado, e em WebAssembly nas visitas seguintes. Isso significa: (1) o componente precisa funcionar nos dois modos, e o carrinho depende de um id que, no Server, não vem do HttpContext. Seria preciso acrescentar a captura no pré-render e a persistência do id, só para cobrir a primeira visita; (2) há duas implementações de serviço ativas em momentos diferentes para o mesmo usuário, com comportamentos sutilmente distintos, como tempo de vida do escopo, tempo real e erros de conexão, que dobram a superfície de testes; (3) toda primeira visita cria um circuito, e os custos de memória e conexão do Server voltam justamente no pico de tráfego de novos visitantes, como uma campanha de marketing. O ganho é de um ou dois segundos de interatividade na primeira visita, enquanto o HTML pré-renderizado já está visível. Para um carrinho, em que o usuário normalmente lê antes de clicar, esse ganho raramente compensa. O Auto brilha em componentes sem estado ligado à requisição, como um filtro de catálogo que só chama endpoints públicos.

Comentários

Mais em Linguagem C#

Operadores, Texto e Conversa com o Usuário
Operadores, Texto e Conversa com o Usuário

Operadores e texto em C#: a divisão inteira que perde a vírgula, o resto com…

Interfaces no .NET: WinForms, WPF, MAUI, Avalonia e o que escolher
Interfaces no .NET: WinForms, WPF, MAUI, Avalonia e o que escolher

Do Windows Forms de 2002 ao .NET MAUI e ao Avalonia: a história das interfaces…

Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos
Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos

A Livraria olhada como um todo: a regra de dependência de Clean Architecture e…