Una de las decisiones fundamentales que deben tomar los desarrolladores web es dónde implementar la lógica y la renderización en su aplicación. Esto puede ser difícil porque hay muchas formas de crear un sitio web.
Nuestro conocimiento de este espacio se basa en el trabajo que realizamos en Chrome con sitios grandes durante los últimos años. En términos generales, recomendamos a los desarrolladores que consideren el procesamiento del servidor o el procesamiento estático en lugar de un enfoque de rehidratación completa.
Para comprender mejor las arquitecturas entre las que elegimos cuando tomamos esta decisión, necesitamos una terminología coherente y un marco de trabajo compartido para cada enfoque. Luego, podrás evaluar mejor las ventajas y desventajas de cada enfoque de renderización desde la perspectiva del rendimiento de la página.
Terminología
Primero, definiremos algunos términos que usaremos.
Renderización
- Renderización del servidor (SSR)
- Renderizar una app en el servidor para enviar HTML, en lugar de JavaScript, al cliente
- Procesamiento del cliente (CSR)
- Renderizar una app en un navegador con JavaScript para modificar el DOM
- Renderización previa
- Ejecutar una aplicación del cliente en el momento de la compilación para capturar su estado inicial como HTML estático
- Hidratación
- Ejecuta secuencias de comandos del cliente para agregar estado de la aplicación e interactividad al HTML renderizado por el servidor. La hidratación supone que el DOM no cambia.
- Rehidratación
- Si bien a menudo se usa con el mismo significado que hidratación, la rehidratación implica actualizar el DOM de forma periódica con el estado más reciente, incluso después de la hidratación inicial.
Rendimiento
- Tiempo hasta el primer byte (TTFB)
- Es el tiempo que transcurre entre el momento en que se hace clic en un vínculo y el momento en que se carga el primer byte de contenido en la página nueva.
- First Contentful Paint (FCP)
- Es la fecha y hora en que se hace visible el contenido solicitado (cuerpo del artículo, etc.).
- Interaction to Next Paint (INP)
- Es una métrica representativa que evalúa si una página responde de forma coherente y rápida a las entradas del usuario.
- Total Blocking Time (TBT)
- Una métrica de proxy para el INP que calcula cuánto tiempo se bloqueó el subproceso principal durante la carga de la página.
Procesamiento del servidor
La renderización del servidor genera el código HTML completo de una página en el servidor en respuesta a la navegación. Esto evita viajes de ida y vuelta adicionales para la recuperación de datos y la generación de plantillas en el cliente, ya que el renderizador los controla antes de que el navegador obtenga una respuesta.
Por lo general, el procesamiento del servidor produce un FCP rápido. Ejecutar la lógica de la página y renderizarla en el servidor te permite evitar enviar mucho código JavaScript al cliente. Esto ayuda a reducir el TTBT de una página, lo que también puede generar un INP más bajo, ya que el subproceso principal no se bloquea con tanta frecuencia durante la carga de la página. Cuando el subproceso principal se bloquea con menos frecuencia, las interacciones del usuario tienen más oportunidades de ejecutarse antes.
Esto tiene sentido, ya que, con la renderización del servidor, solo envías texto y vínculos al navegador del usuario. Este enfoque puede funcionar bien en una variedad de condiciones de dispositivos y redes, y abre interesantes optimizaciones del navegador, como el análisis de documentos de transmisión.
 
  Con la renderización del servidor, es menos probable que los usuarios deban esperar a que se ejecute el código JavaScript vinculado a la CPU antes de poder usar tu sitio. Incluso cuando no puedes evitar el JavaScript de terceros, usar el procesamiento del servidor para reducir tus propios costos de JavaScript de origen puede brindarte más presupuesto para el resto. Sin embargo, este enfoque tiene una posible desventaja: generar páginas en el servidor lleva tiempo, lo que puede aumentar el TTFB de tu página.
Si la renderización del servidor es suficiente para tu aplicación, depende en gran medida del tipo de experiencia que estés creando. Existe un debate de larga data sobre las aplicaciones correctas de la renderización del servidor en comparación con la renderización del cliente, pero siempre puedes optar por usar la renderización del servidor para algunas páginas y no para otras. Algunos sitios adoptaron técnicas de renderización híbrida con éxito. Por ejemplo, Netflix renderiza en el servidor sus páginas de destino relativamente estáticas, mientras que prefetching el código JavaScript para las páginas con mucha interacción, lo que les da a estas páginas más pesadas renderizadas en el cliente una mejor oportunidad de cargarse rápidamente.
Con muchos frameworks, bibliotecas y arquitecturas modernos, puedes renderizar la misma aplicación tanto en el cliente como en el servidor. Puedes usar estas técnicas para la renderización del servidor. Sin embargo, las arquitecturas en las que la renderización se realiza tanto en el servidor como en el cliente son su propia clase de solución con características de rendimiento y compensaciones muy diferentes. Los usuarios de React pueden usar APIs de DOM del servidor o soluciones creadas sobre ellas, como Next.js, para la renderización del servidor. Los usuarios de Vue pueden usar la guía de renderización del servidor de Vue o Nuxt. Angular tiene Universal.
Sin embargo, la mayoría de las soluciones populares usan alguna forma de hidratación, por lo que debes tener en cuenta los enfoques que usa tu herramienta.
Renderización estática
La renderización estática se realiza en el momento de la compilación. Este enfoque ofrece un FCP rápido y también un TBT y un INP más bajos, siempre y cuando limites la cantidad de JavaScript del cliente en tus páginas. A diferencia de la renderización del servidor, también logra un TTFB rápido de forma constante, ya que el HTML de una página no se tiene que generar de forma dinámica en el servidor. En general, la renderización estática significa producir un archivo HTML separado para cada URL con anticipación. Con las respuestas en HTML generadas con anticipación, puedes implementar renderizaciones estáticas en varias CDN para aprovechar el almacenamiento en caché perimetral.
 
  Las soluciones para la renderización estática vienen en todas las formas y tamaños. Herramientas como Gatsby están diseñadas para que los desarrolladores sientan que su aplicación se renderiza de forma dinámica, no que se genera como un paso de compilación. Las herramientas de generación de sitios estáticos, como 11ty, Jekyll y Metalsmith, adoptan su naturaleza estática y proporcionan un enfoque más basado en plantillas.
Una de las desventajas de la renderización estática es que debe generar archivos HTML individuales para cada URL posible. Esto puede ser difícil o incluso inviable cuando necesitas predecir esas URLs con anticipación y para sitios con una gran cantidad de páginas únicas.
Los usuarios de React tal vez conozcan Gatsby, la exportación estática de Next.js o Navi, que facilitan la creación de páginas a partir de componentes. Sin embargo, el procesamiento estático y el procesamiento previo se comportan de manera diferente: las páginas renderizadas de forma estática son interactivas sin necesidad de ejecutar mucho JavaScript del cliente, mientras que el procesamiento previo mejora el FCP de una aplicación de una sola página que debe iniciarse en el cliente para que las páginas sean verdaderamente interactivas.
Si no sabes con certeza si una solución determinada es renderización estática o previa, inhabilita JavaScript y carga la página que deseas probar. En el caso de las páginas renderizadas de forma estática, la mayoría de las funciones interactivas siguen existiendo sin JavaScript. Es posible que las páginas renderizadas previamente aún tengan algunas funciones básicas, como vínculos con JavaScript inhabilitado, pero la mayor parte de la página es inerte.
Otra prueba útil es usar la limitación de red en Chrome DevTools y ver cuánto JavaScript se descarga antes de que una página se vuelva interactiva. Por lo general, la renderización previa necesita más JavaScript para volverse interactiva, y ese JavaScript tiende a ser más complejo que el enfoque de mejora progresiva que se usa en la renderización estática.
Comparación entre el procesamiento del servidor y el procesamiento estático
La renderización del servidor no es la mejor solución para todo, ya que su naturaleza dinámica puede generar costos significativos de sobrecarga de procesamiento. Muchas soluciones de renderización del servidor no vacían los datos de forma anticipada, retrasan el TTFB o duplican los datos que se envían (por ejemplo, los estados intercalados que usa JavaScript en el cliente). En React, renderToString() puede ser lento porque es síncrono y de un solo subproceso.
Las APIs de DOM del servidor de React más recientes admiten la transmisión, lo que permite que la parte inicial de una respuesta HTML llegue al navegador antes mientras el resto se sigue generando en el servidor.
Lograr que la renderización del servidor sea "correcta" puede implicar encontrar o crear una solución para el almacenamiento en caché de componentes, administrar el consumo de memoria, usar técnicas de memoización y otras consideraciones. A menudo, procesas o recompilas la misma app dos veces, una en el cliente y otra en el servidor. El procesamiento del servidor que muestra el contenido antes no necesariamente implica menos trabajo para ti. Si tienes mucho trabajo en el cliente después de que llega al cliente una respuesta HTML generada por el servidor, esto puede generar un TBT y un INP más altos para tu sitio web.
La renderización del servidor produce HTML a pedido para cada URL, pero puede ser más lenta que solo entregar contenido renderizado estático. Si puedes realizar el trabajo adicional, la renderización del servidor y el almacenamiento en caché de HTML pueden reducir significativamente el tiempo de renderización del servidor. La ventaja de la renderización del servidor es la capacidad de extraer más datos "en vivo" y responder a un conjunto más completo de solicitudes de lo que es posible con la renderización estática. Las páginas que necesitan personalización son un ejemplo concreto del tipo de solicitud que no funciona bien con la renderización estática.
La renderización del servidor también puede presentar decisiones interesantes cuando se compila una PWA. ¿Es mejor usar el almacenamiento en caché del service worker de página completa o renderizar individualmente en el servidor cada fragmento de contenido?
Procesamiento del cliente
El procesamiento del cliente significa que las páginas se procesan directamente en el navegador con JavaScript. Toda la lógica, la recuperación de datos, la creación de plantillas y el enrutamiento se controlan en el cliente en lugar de en el servidor. El resultado efectivo es que se pasan más datos del servidor al dispositivo del usuario, lo que conlleva su propio conjunto de compensaciones.
La renderización del cliente puede ser difícil de hacer y mantener rápida para los dispositivos móviles.
Con un poco de trabajo para mantener un presupuesto de JavaScript ajustado y ofrecer valor en la menor cantidad posible de viajes de ida y vuelta, puedes lograr que la renderización del cliente casi replique el rendimiento de la renderización pura del servidor. Puedes hacer que el analizador funcione más rápido para ti si entregas secuencias de comandos y datos críticos con <link rel=preload>. También recomendamos considerar el uso de patrones como PRPL para garantizar que las navegaciones iniciales y posteriores se sientan instantáneas.
 
  El principal inconveniente de la renderización del cliente es que la cantidad de JavaScript que se requiere tiende a aumentar a medida que crece una aplicación, lo que puede afectar el INP de una página. Esto se vuelve especialmente difícil con la incorporación de nuevas bibliotecas de JavaScript, polyfills y código de terceros, que compiten por la capacidad de procesamiento y, a menudo, deben procesarse antes de que se pueda renderizar el contenido de una página.
Las experiencias que usan la renderización del cliente y dependen de paquetes grandes de JavaScript deben considerar la división agresiva del código para reducir el TBT y el INP durante la carga de la página, así como la carga diferida de JavaScript para mostrar solo lo que el usuario necesita, cuando lo necesita. Para las experiencias con poca o ninguna interactividad, la renderización del servidor puede representar una solución más escalable para estos problemas.
Para las personas que compilan aplicaciones de una sola página, identificar las partes principales de la interfaz de usuario que comparten la mayoría de las páginas permite aplicar la técnica de almacenamiento en caché de la shell de la aplicación. En combinación con los service workers, esto puede mejorar drásticamente el rendimiento percibido en las visitas repetidas, ya que la página puede cargar su HTML de shell de aplicación y sus dependencias desde CacheStorage muy rápidamente.
La rehidratación combina el procesamiento del servidor y el procesamiento del cliente
La hidratación es un enfoque que mitiga las desventajas del procesamiento del cliente y del servidor, ya que realiza ambos. Las solicitudes de navegación, como las cargas o las recargas de páginas completas, se controlan mediante un servidor que renderiza la aplicación en HTML. Luego, el código JavaScript y los datos que se usan para la renderización se incorporan en el documento resultante. Cuando se hace con cuidado, se logra un FCP rápido, como la renderización del servidor, y, luego, se "retoma" con la renderización en el cliente.
Esta es una solución eficaz, pero puede tener inconvenientes de rendimiento considerables.
El principal inconveniente de la renderización del servidor con rehidratación es que puede tener un impacto negativo significativo en el TBT y el INP, incluso si mejora el FCP. Las páginas renderizadas del servidor pueden parecer cargadas e interactivas, pero en realidad no pueden responder a la entrada hasta que se ejecutan las secuencias de comandos del cliente para los componentes y se adjuntan los controladores de eventos. En dispositivos móviles, esto puede tardar minutos, lo que confunde y frustra al usuario.
Un problema de rehidratación: una app por el precio de dos
Para que el JavaScript del cliente se haga cargo con precisión de donde lo dejó el servidor, sin volver a solicitar todos los datos con los que el servidor renderizó su HTML, la mayoría de las soluciones de renderización del servidor serializan la respuesta de las dependencias de datos de una IU como etiquetas de secuencia de comandos en el documento. Debido a que esto duplica una gran cantidad de HTML, la rehidratación puede causar más problemas que solo la interactividad retrasada.
 
  El servidor devuelve una descripción de la IU de la aplicación en respuesta a una solicitud de navegación, pero también devuelve los datos de origen que se usaron para componer esa IU y una copia completa de la implementación de la IU que luego se inicia en el cliente. La IU no se vuelve interactiva hasta que bundle.js termina de cargarse y ejecutarse.
Las métricas de rendimiento recopiladas de sitios web reales que usan la renderización del servidor y la rehidratación indican que rara vez es la mejor opción. El motivo más importante es su efecto en la experiencia del usuario, cuando una página parece lista, pero ninguna de sus funciones interactivas funciona.
 
  Hay esperanzas para la renderización del servidor con rehidratación. A corto plazo, usar solo la renderización del servidor para el contenido altamente almacenable en caché puede reducir el TTFB y producir resultados similares a la renderización previa. La rehidratación incremental, progresiva o parcial podría ser la clave para que esta técnica sea más viable en el futuro.
Realiza el procesamiento del servidor de forma progresiva y rehidrata el contenido
La renderización del servidor tuvo varios desarrollos en los últimos años.
La renderización del servidor de transmisión te permite enviar HTML en fragmentos que el navegador puede renderizar de forma progresiva a medida que se reciben. Esto puede hacer que el lenguaje de marcado llegue a tus usuarios más rápido, lo que acelera tu FCP. En React, el hecho de que las transmisiones sean asíncronas en renderToPipeableStream(), en comparación con renderToString() síncrono, significa que la contrapresión se maneja bien.
También vale la pena considerar la rehidratación progresiva (React la implementó). Con este enfoque, las partes individuales de una aplicación renderizada por el servidor se "inician" con el tiempo, en lugar del enfoque común actual de inicializar toda la aplicación de una vez. Esto puede ayudar a reducir la cantidad de JavaScript necesario para que las páginas sean interactivas, ya que te permite aplazar la actualización del cliente de las partes de baja prioridad de la página para evitar que bloquee el subproceso principal, lo que permite que las interacciones del usuario se produzcan antes después de que el usuario las inicie.
La rehidratación progresiva también puede ayudarte a evitar uno de los errores más comunes de la rehidratación de la renderización del servidor: se destruye un árbol DOM renderizado por el servidor y, luego, se vuelve a compilar de inmediato, la mayoría de las veces porque la renderización inicial síncrona del cliente requería datos que aún no estaban listos, a menudo un Promise que aún no se resolvió.
Rehidratación parcial
Se demostró que la rehidratación parcial es difícil de implementar. Este enfoque es una extensión de la rehidratación progresiva que analiza partes individuales de la página (componentes, vistas o árboles) y, luego, identifica las partes con poca interactividad o sin reactividad. Para cada una de estas partes casi estáticas, el código JavaScript correspondiente se transforma en referencias inertes y funciones decorativas, lo que reduce su huella del lado del cliente a casi cero.
El enfoque de rehidratación parcial tiene sus propios problemas y limitaciones. Esto plantea algunos desafíos interesantes para el almacenamiento en caché, y la navegación del cliente significa que no podemos suponer que el HTML renderizado por el servidor para las partes inertes de la aplicación está disponible sin una carga de página completa.
Renderización trisomórfica
Si los service workers son una opción para ti, considera la renderización trisomórfica. Esta técnica te permite usar la renderización del servidor de transmisión para las navegaciones iniciales o que no son de JavaScript, y, luego, hacer que tu Service Worker se encargue de renderizar el HTML para las navegaciones después de que se haya instalado. Esto puede mantener actualizados los componentes y las plantillas almacenados en caché, y habilitar navegaciones de estilo SPA para renderizar vistas nuevas en la misma sesión. Este enfoque funciona mejor cuando puedes compartir el mismo código de plantillas y de enrutamiento entre el servidor, la página del cliente y el service worker.
 
  Consideraciones de SEO
Cuando eligen una estrategia de renderización web, los equipos suelen tener en cuenta el impacto en la SEO. La renderización del servidor es una opción popular para ofrecer una experiencia de aspecto "completo" que los rastreadores puedan interpretar. Los rastreadores pueden comprender JavaScript, pero a menudo existen limitaciones en la forma en que renderizan. La renderización del cliente puede funcionar, pero a menudo necesita pruebas y sobrecarga adicionales. Más recientemente, la renderización dinámica también se convirtió en una opción que vale la pena considerar si tu arquitectura depende en gran medida de JavaScript del cliente.
En caso de duda, la herramienta de prueba de optimización para dispositivos móviles es una excelente manera de verificar que el enfoque elegido haga lo que esperas. Muestra una vista previa visual de cómo aparece cualquier página para el rastreador de Google, el contenido HTML serializado que encuentra después de que se ejecuta JavaScript y los errores que se producen durante el procesamiento.
 
  Conclusión
Cuando decidas un enfoque de renderización, mide y comprende cuáles son tus cuellos de botella. Considera si el procesamiento estático o el procesamiento del servidor pueden ayudarte a lograr la mayor parte del objetivo. Está bien enviar principalmente HTML con un mínimo de JavaScript para lograr una experiencia interactiva. Esta es una infografía útil que muestra el espectro cliente-servidor:
 
  Créditos {:#credits}
Gracias a todos por sus opiniones y por inspirarnos:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson y Sebastian Markbåge.
 
 
        
        