{"id":300,"date":"2026-05-09T22:21:19","date_gmt":"2026-05-10T02:21:19","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=300"},"modified":"2026-05-09T22:29:43","modified_gmt":"2026-05-10T02:29:43","slug":"blazor-web-apps-en-profundidad-estado-ciclo-de-vida-y-pitfalls","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/05\/blazor-web-apps-en-profundidad-estado-ciclo-de-vida-y-pitfalls\/","title":{"rendered":"Blazor Web Apps en profundidad: Estado, ciclo de vida y pitfalls"},"content":{"rendered":"\n<p>En el <a href=\"https:\/\/juredev.com\/blog\/2026\/05\/blazor-web-apps-elegir-y-aplicar-render-modes\/\">art\u00edculo anterior<\/a> vimos c\u00f3mo elegir y aplicar los diferentes <strong>Render Modes<\/strong>. Parec\u00eda bastante sencillo: pones <code>@rendermode InteractiveAuto<\/code> y listo. Pero en cuanto empiezas a construir componentes reales, con llamadas a bases de datos, APIs o JavaScript, aparece el primer susto:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u00abMi componente est\u00e1 ejecutando las peticiones dos veces\u2026 y no es un bug.\u00bb<\/p>\n<\/blockquote>\n\n\n\n<p>En esta entrega vamos a entender por qu\u00e9 ocurren estos problemas, c\u00f3mo se comporta el estado en este nuevo modelo h\u00edbrido y, lo m\u00e1s importante, c\u00f3mo solucionarlos con las herramientas que nos ofrece .NET 10.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Checklist r\u00e1pida<\/h2>\n\n\n\n<p>Si ya tienes experiencia con Blazor y quieres saber si este art\u00edculo te va a servir, aqu\u00ed va la esencia:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Si usas<\/strong> <code>Auto<\/code> &#8211;> Implementa siempre <code>PersistentComponentState<\/code> cuando hagas llamadas a bases de datos o APIs.<\/li>\n\n\n\n<li><strong>Si llamas a JavaScript<\/strong> &#8211;> Hazlo solo en <code>OnAfterRenderAsync<\/code>.<\/li>\n\n\n\n<li><strong>Si usas<\/strong> <code>Server<\/code> &#8211;> Monitorea el consumo de RAM en producci\u00f3n: los singletons son compartidos entre todos los usuarios.<\/li>\n\n\n\n<li><strong>Si fallan los par\u00e1metros<\/strong> &#8211;> Revisa que est\u00e9s enviando solo objetos serializables entre render modes.<\/li>\n\n\n\n<li><strong>Si necesitas saber d\u00f3nde est\u00e1s ejecut\u00e1ndote <\/strong>&#8211;> Usa <code>RendererInfo<\/code> de forma defensiva antes de tocar APIs del navegador.<\/li>\n<\/ul>\n\n\n\n<p>Si alguno de estos puntos te genera dudas, sigue leyendo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. El problema oculto del prerendering y el ciclo de vida <\/h2>\n\n\n\n<p>Cuando usamos cualquier modo interactivo (<code>Server<\/code>, <code>WebAssembly<\/code> o <code>Auto<\/code>), Blazor activa por defecto el <strong>prerendering<\/strong>. El servidor ejecuta tu componente, genera el HTML est\u00e1tico y lo env\u00eda al navegador para que el usuario vea la interfaz inmediatamente.<\/p>\n\n\n\n<p>Poco despu\u00e9s, la interactividad se activa (ya sea mediante SignalR o WebAssembly) y el componente <strong>se reinicializa<\/strong> en su nuevo entorno. Esto es lo que genera la doble ejecuci\u00f3n que tanto confunde al principio.<\/p>\n\n\n\n<p>El flujo completo con <code>InteractiveAuto<\/code> se ve as\u00ed:<\/p>\n\n\n\n<script type=\"module\">\n  import mermaid from 'https:\/\/cdn.jsdelivr.net\/npm\/mermaid@10\/dist\/mermaid.esm.min.mjs';\n  mermaid.initialize({ \n    startOnLoad: true, \n    theme: 'dark'\n  });\n<\/script>\n<div style=\"display: flex; justify-content: center; align-items: center; width: 100%; margin: 20px 0;\">\n    <pre class=\"mermaid\" style=\"background: transparent; border: none; color: transparent;\">\nflowchart TD\n    A([\"\ud83c\udf10 Petici\u00f3n inicial del usuario\"])\n\n    subgraph SSR[\"Fase 1 \u2014 Static SSR (Prerender)\"]\n        B[\"Servidor ejecuta el componente\\nGenera HTML est\u00e1tico\"]\n        C([\"OnInitializedAsync se ejecuta\\n1\u00aa vez en servidor\"])\n        D[\"HTML + estado serializado\\nenviado al navegador\"]\n    end\n\n    subgraph SERVER[\"Fase 2 \u2014 Interactive Server (1\u00aa visita)\"]\n        E[\"WebSocket SignalR se activa\\nComponente se reinicializa\"]\n        F([\"OnInitializedAsync se ejecuta\\n2\u00aa vez via SignalR\"])\n        G[\"Estado recuperado desde HTML\\nsin nueva llamada a la API\"]\n    end\n\n    subgraph WASM[\"Fase 3 \u2014 Interactive WASM (visitas futuras)\"]\n        H[\".NET WASM descargado\\nen background\"]\n        I([\"OnInitializedAsync corre\\nen el browser del cliente\"])\n        J[\"Sin servidor necesario\\nPura ejecuci\u00f3n en cliente\"]\n    end\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n    F --> G\n    G -.->|\"Bundle WASM\\ndescargado en bg\"| H\n    H --> I\n    I --> J\n    <\/pre>\n<\/div>\n\n\n\n<h3 class=\"wp-block-heading\"><code>OnInitializedAsync<\/code> se ejecuta dos veces<\/h3>\n\n\n\n<p>Si dentro de <code>OnInitializedAsync<\/code> haces <code>await _api.GetData()<\/code>, esa llamada se ejecutar\u00e1 una vez en el servidor (prerender) y otra cuando se active la interactividad. El resultado: doble carga, posible flickering y experiencia degradada.<\/p>\n\n\n\n<p>La soluci\u00f3n oficial y elegante de Microsoft es <code>PersistentComponentState<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Manejo de estado: PersistentComponentState en acci\u00f3n<\/h2>\n\n\n\n<p>La idea es sencilla pero poderosa: durante el prerender guardamos el estado en el HTML y, cuando el componente interactivo despierta, lo recupera directamente en lugar de volver a consultar la fuente de datos.<\/p>\n\n\n\n<p>As\u00ed lo aplicamos en la p\u00e1gina <code>\/analytics<\/code> de nuestra <strong>Task Manager App<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@page \"\/analytics\"\n@using TaskManagerApp.Client.Models\n@using TaskManagerApp.Client.Services\n@inject ITaskService TaskService\n@inject PersistentComponentState ApplicationState\n@implements IDisposable\n@rendermode InteractiveAuto\n\n@* ... HTML de la p\u00e1gina ... *@\n\n@code {\n    private List&lt;TaskItem>? tasks;\n    private PersistingComponentStateSubscription persistingSubscription;\n\n    protected override async Task OnInitializedAsync()\n    {\n        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);\n\n        if (!ApplicationState.TryTakeFromJson&lt;List&lt;TaskItem>>(\"tasks\", out var restored))\n        {\n            \/\/ Primera vez (prerender) \u2192 vamos a buscar los datos\n            tasks = await TaskService.GetTasksAsync();\n        }\n        else\n        {\n            \/\/ Al activar interactividad \u2192 recuperamos el estado ya serializado\n            tasks = restored;\n        }\n    }\n\n    private Task PersistData()\n    {\n        if (tasks != null)\n        {\n            ApplicationState.PersistAsJson(\"tasks\", tasks);\n        }\n        return Task.CompletedTask;\n    }\n\n    public void Dispose()\n    {\n        persistingSubscription.Dispose();\n    }\n}<\/code><\/pre>\n\n\n\n<p><strong>Trade-off importante<\/strong>: No abuses de esto. Guardar objetos muy pesados aumenta el tama\u00f1o del HTML inicial. \u00dasalo para datos cr\u00edticos de carga inicial y deja las listas grandes para carga bajo demanda o paginaci\u00f3n.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. RendererInfo: \u00bfD\u00f3nde diablos estoy ejecut\u00e1ndome?<\/h2>\n\n\n\n<p>Con <code>InteractiveAuto<\/code> tu c\u00f3digo puede ejecutarse en tres contextos distintos: servidor est\u00e1tico (prerender), servidor interactivo (SignalR) y cliente (WebAssembly). A veces necesitas saber exactamente d\u00f3nde est\u00e1s.<\/p>\n\n\n\n<p>.NET 9+ nos da <code>RendererInfo<\/code> para esto.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Uso para depuraci\u00f3n<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"alert alert-info\">\n    &lt;strong>Entorno actual:&lt;\/strong> @RendererInfo.Name &lt;br \/>\n    &lt;strong>Interactivo:&lt;\/strong> @(RendererInfo.IsInteractive ? \"S\u00ed\" : \"No (Prerendering)\")\n&lt;\/div><\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Uso defensivo (el realmente \u00fatil)<\/h3>\n\n\n\n<p>Evita errores cl\u00e1sicos como intentar acceder a <code>localStorage<\/code> durante el prerender:<\/p>\n\n\n\n<p><strong>Malo (falla en prerender):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@code {\n    protected override async Task OnInitializedAsync()\n    {\n        \/\/ System.InvalidOperationException: JavaScript interop calls cannot be issued at this time.\n        var theme = await JS.InvokeAsync&lt;string>(\"localStorage.getItem\", \"theme\");\n    }\n}<\/code><\/pre>\n\n\n\n<p><strong>Versi\u00f3n segura con<\/strong> <code>RendererInfo<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@code {\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (firstRender &amp;&amp; RendererInfo.IsInteractive)\n        {\n            \/\/ Solo llegamos aqu\u00ed cuando hay un browser real ejecutando el c\u00f3digo\n            var theme = await JS.InvokeAsync&lt;string>(\"localStorage.getItem\", \"theme\");\n            ApplyTheme(theme);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p><strong>Regla de oro<\/strong>: <code>RendererInfo.IsInteractive<\/code> es tu guardia de seguridad antes de cualquier operaci\u00f3n que requiera un navegador real.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Pitfalls importantes (d\u00f3nde m\u00e1s vas a tropezar)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Jerarqu\u00edas con distintos render modes<\/h3>\n\n\n\n<p>Un componente hijo con <code>InteractiveWebAssembly<\/code> no puede recibir por par\u00e1metro un objeto complejo (como una instancia de base de datos o un servicio con dependencias no serializables) desde un componente padre en modo Static SSR. Los par\u00e1metros que cruzan entre SSR e Interactivo deben ser <strong>serializables a JSON<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Llamadas a JavaScript fuera de <code>OnAfterRenderAsync<\/code><\/h3>\n\n\n\n<p>Este es uno de los errores m\u00e1s comunes y frustrantes:<\/p>\n\n\n\n<p><strong>C\u00f3digo que falla<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>protected override async Task OnInitializedAsync()\n{\n    await JS.InvokeVoidAsync(\"miFuncion\"); \/\/ Explota en prerender\n}<\/code><\/pre>\n\n\n\n<p><strong>C\u00f3digo correcto<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>protected override async Task OnAfterRenderAsync(bool firstRender)\n{\n    if (firstRender)\n    {\n        await JS.InvokeVoidAsync(\"miFuncion\"); \/\/ Seguro: el DOM ya existe\n    }\n}<\/code><\/pre>\n\n\n\n<p><strong>Regla de oro<\/strong>: Cualquier llamada a JavaScript (<code>IJSRuntime<\/code>) debe hacerse en <code>OnAfterRenderAsync(bool firstRender)<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Estado en InteractiveServer<\/h3>\n\n\n\n<p>Aunque <code>InteractiveServer<\/code> es muy r\u00e1pido, todo el estado vive en la memoria RAM del servidor mientras la conexi\u00f3n WebSocket permanezca abierta.<\/p>\n\n\n\n<p>En la vista de <code>\/tasks<\/code> de nuestro proyecto dejamos esta nota recordatorio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>protected override async Task OnInitializedAsync()\n{\n    \/\/ En InteractiveServer, el estado se mantiene en memoria en el servidor para esta conexi\u00f3n SignalR.\n    \/\/ Ojo: Si el usuario recarga la p\u00e1gina, esta instancia del componente se destruye y el estado interno se pierde.\n    tasks = await TaskService.GetTasksAsync();\n}<\/code><\/pre>\n\n\n\n<p>Si el servidor se reinicia o la conexi\u00f3n se pierde, los usuarios pierden su estado. Siempre dise\u00f1a pensando que la conexi\u00f3n SignalR puede romperse en cualquier momento.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Performance en el mundo real<\/h2>\n\n\n\n<p>Cuando aplicas correctamente el manejo de estado y ciclo de vida, obtienes mejoras reales de rendimiento:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>WASM Payload<\/strong>: Con <code>Auto<\/code>, el peso inicial de .NET no penaliza al usuario porque la primera carga se renderiza instant\u00e1neamente desde el servidor gracias al prerendering. El bundle de WASM se descarga en segundo plano.<\/li>\n\n\n\n<li><strong>SignalR Scaling<\/strong>: Al combinar p\u00e1ginas Static SSR con solo algunas p\u00e1ginas Interactive Server, reduces significativamente el consumo de memoria del servidor. No todos los usuarios necesitan un WebSocket abierto.<\/li>\n\n\n\n<li><strong>Caching<\/strong>: En Static SSR el Output Caching tradicional vuelve a ser extremadamente \u00fatil con <code>[OutputCache]<\/code>. Esto no es posible en componentes InteractiveServer porque son stateful.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">6. Reglas Pr\u00e1cticas Finales<\/h2>\n\n\n\n<p>Para que no tengas que recordar todo esto cada d\u00eda, aqu\u00ed tienes la checklist completa (disponible tambi\u00e9n al principio del art\u00edculo):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Si usas<\/strong> <code>Auto<\/code> &#8211;> Implementa siempre <code>PersistentComponentState<\/code> si tienes llamadas a bases de datos o APIs lentas.<\/li>\n\n\n\n<li><strong>Si llamas a JS<\/strong> &#8211;> Hazlo exclusivamente en <code>OnAfterRenderAsync<\/code> y prot\u00e9gelo con <code>firstRender<\/code> o <code>RendererInfo.IsInteractive<\/code>.<\/li>\n\n\n\n<li><strong>Si usas<\/strong> <code>Server<\/code> &#8211;> Monitoriza el consumo de RAM en producci\u00f3n y recuerda que los singletons son compartidos entre todos los usuarios.<\/li>\n\n\n\n<li><strong>Si fallan los par\u00e1metros<\/strong> &#8211;> Revisa si est\u00e1s enviando objetos no serializables desde un render mode a otro.<\/li>\n\n\n\n<li><strong>Si necesitas APIs de browser<\/strong> &#8211;> Usa <code>RendererInfo.IsInteractive<\/code> como guardia defensiva antes de llamarlas.<\/li>\n<\/ul>\n\n\n\n<p>Con estas bases t\u00e9cnicas tu aplicaci\u00f3n Blazor ser\u00e1 mucho m\u00e1s r\u00e1pida, estable y predecible.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Y ahora que viene?<\/h2>\n\n\n\n<p>En el pr\u00f3ximo art\u00edculo de la serie subiremos un nivel m\u00e1s y hablaremos de \u201cArquitectura real con Blazor Web Apps\u201d: c\u00f3mo organizar proyectos grandes, estructurar servicios que funcionen correctamente en todos los render modes y manejar autenticaci\u00f3n de forma robusta.<\/p>\n\n\n\n<p>Todos los ejemplos de este art\u00edculo est\u00e1n extra\u00eddos de la Task Manager App, la aplicaci\u00f3n de referencia que acompa\u00f1a esta serie. Si quieres explorar el c\u00f3digo completo, ver c\u00f3mo encajan todas las piezas o usarlo como base para tus proyectos, el repositorio est\u00e1 disponible en GitHub: <a href=\"https:\/\/github.com\/jure-ve\/TaskManagerApp\">TaskManagerApp<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>En el art\u00edculo anterior vimos c\u00f3mo elegir y aplicar los diferentes Render Modes. Parec\u00eda bastante sencillo: pones @rendermode InteractiveAuto y listo. Pero en cuanto empiezas a construir componentes reales, con llamadas a bases de datos, APIs o JavaScript, aparece el primer susto: \u00abMi componente est\u00e1 ejecutando las peticiones dos veces\u2026 y no es un bug.\u00bb [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[26,11],"class_list":["post-300","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-net","tag-c"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/300","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=300"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/300\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=300"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=300"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=300"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}