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
- Microsoft Learn — ASP.NET Core Blazor render modes: https://learn.microsoft.com/aspnet/core/blazor/components/render-modes
- Microsoft Learn — Prerender ASP.NET Core Razor components (persistência de estado): https://learn.microsoft.com/aspnet/core/blazor/components/prerender
- Microsoft Learn — ASP.NET Core Blazor state management: https://learn.microsoft.com/aspnet/core/blazor/state-management
- Microsoft Learn — ASP.NET Core Blazor dependency injection (tempos de vida por modo): https://learn.microsoft.com/aspnet/core/blazor/fundamentals/dependency-injection
- Microsoft Learn — IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps: https://learn.microsoft.com/aspnet/core/blazor/components/httpcontext
- WebAssembly — especificação e visão geral: https://webassembly.org/
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.