Renderización en la Web

Una de las decisiones clave 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.

Nuestra comprensión de este espacio se basa en nuestro trabajo 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 hidratación completa.

Para comprender mejor las arquitecturas entre las que elegimos cuando tomamos esta decisión, necesitamos una comprensión sólida de cada enfoque y una terminología consistente para usar cuando hablemos de ellos. Las diferencias entre los enfoques de renderización ayudan a ilustrar las compensaciones de la renderización en la Web desde la perspectiva del rendimiento de la página.

Terminología

Primero, definamos algunos términos que usaremos.

Renderización

Renderización del servidor (SSR)
Renderiza una app en el servidor para enviar HTML, en lugar de JavaScript, al cliente.
Renderización del cliente (CSR)
Renderización de una app en un navegador con JavaScript para modificar el DOM.
Rehidratación
"Inicio" de vistas de JavaScript en el cliente para que reutilicen el árbol y los datos del DOM del HTML renderizado por el servidor.
Renderización previa
Ejecuta una aplicación del cliente en el tiempo de compilación para capturar su estado inicial como HTML estático.

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.
Primer procesamiento de imagen con contenido (FCP)
Es el momento en que el contenido solicitado (cuerpo del artículo, etcétera) se hace visible.
Interaction to Next Paint (INP)
Es una métrica representativa que evalúa si una página responde de forma rápida y coherente a las entradas del usuario.
Tiempo de bloqueo total (TBT)
Es una métrica de proxy para INP que calcula durante cuánto tiempo se bloqueó el subproceso principal durante la carga de la página.

Renderización 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 creación de plantillas en el cliente, ya que el renderizador los controla antes de que el navegador reciba una respuesta.

Por lo general, el procesamiento del servidor produce un FCP rápido. Ejecutar la lógica de la página y la renderización en el servidor te permite evitar enviar mucho JavaScript al cliente. Esto ayuda a reducir el TBT 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 para una variedad de condiciones de dispositivos y redes, y abre optimizaciones interesantes del navegador, como el análisis de documentos de transmisión.

Diagrama que muestra la renderización del servidor y la ejecución de JS que afectan el FCP y el TTI.
FCP y TTI con renderización del servidor.

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 JS de terceros, usar la renderización del servidor para reducir tus propios costos de JavaScript puede darte 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 compilando. 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 con éxito técnicas de renderización híbrida. Por ejemplo, Netflix renderiza sus páginas de destino relativamente estáticas en el servidor, mientras que prefetching el JS para las páginas con mucha interacción, lo que les brinda a estas páginas renderizadas por el cliente más pesadas una mejor oportunidad de cargarse rápidamente.

Muchos frameworks, bibliotecas y arquitecturas modernos te permiten renderizar la misma aplicación en el cliente y el servidor. Puedes usar estas técnicas para la renderización del servidor. Sin embargo, las arquitecturas en las que la renderización se produce en el servidor y en el cliente son su propia clase de solución con características y compensaciones de rendimiento muy diferentes. Los usuarios de React pueden usar APIs de DOM del servidor o soluciones compiladas en 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 algún tipo de hidratación, así que ten en cuenta los enfoques que usa tu herramienta.

Renderización estática

La renderización estática ocurre en el tiempo de compilación. Este enfoque ofrece un FCP rápido y también un TBT y una INP más bajos, siempre que limites la cantidad de JS del cliente en tus páginas. A diferencia de la renderización del servidor, también logra un TTFB rápido y constante, ya que el código HTML de una página no tiene que generarse de forma dinámica en el servidor. En general, la renderización estática implica producir un archivo HTML independiente para cada URL con anticipación. Con las respuestas HTML generadas con anticipación, puedes implementar renderizaciones estáticas en varias CDN para aprovechar el almacenamiento en caché en el perímetro.

Diagrama que muestra la renderización estática y la ejecución opcional de JS que afectan el FCP y el TTI.
FCP y TTI con renderización estática.

Las soluciones para la renderización estática pueden ser de diferentes formas y tamaños. Las herramientas como Gatsby están diseñadas para que los desarrolladores sientan que su aplicación se renderiza de forma dinámica, no 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 un desafío o incluso inviable cuando no puedes predecir cuáles serán esas URLs con anticipación o para sitios con una gran cantidad de páginas únicas.

Es posible que los usuarios de React estén familiarizados con 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, la renderización estática y la renderización previa 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 la renderización previa mejora el FCP de una aplicación de una sola página que se debe iniciar en el cliente para que las páginas sean realmente interactivas.

Si no sabes si una solución determinada es de renderización estática o previa, intenta inhabilitar 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 aún existen sin JavaScript. Es posible que las páginas renderizadas previamente sigan teniendo 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 la red en Chrome DevTools y ver cuántas descargas de JavaScript se realizan antes de que una página se vuelva interactiva. Por lo general, la renderización previa necesita más JavaScript para que sea 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.

Renderización del servidor en comparación con la renderización estática

La renderización del servidor no es la mejor solución para todo, ya que su naturaleza dinámica puede tener costos significativos de sobrecarga de procesamiento. Muchas soluciones de renderización del servidor no borran los datos con anticipación, retrasan el TTFB ni 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, que puede enviar la parte inicial de una respuesta HTML al navegador antes, mientras que el resto aún se genera en el servidor.

Para que la renderización del servidor sea "correcta", es posible que debas encontrar o crear una solución para la caché de componentes, administrar el consumo de memoria, usar técnicas de memoización y otras inquietudes. A menudo, procesas o vuelves a compilar la misma app dos veces, una en el cliente y otra en el servidor. La renderización del servidor que muestra el contenido antes no necesariamente te da menos trabajo. Si tienes mucho trabajo en el cliente después de que llega 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 la entrega de contenido renderizado estático. Si puedes realizar el trabajo adicional, la renderización del servidor más la 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 que 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 crea una PWA: ¿es mejor usar el almacenamiento en caché de trabajador de servicio de página completa o simplemente renderizar contenido individual del servidor?

Renderización del cliente

La renderización del cliente consiste en renderizar páginas 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 eficaz es que se pasan más datos al dispositivo del usuario desde el servidor, lo que tiene sus propias compensaciones.

La renderización del cliente puede ser difícil de realizar y mantener rápida para los dispositivos móviles. Con un poco de trabajo para mantener un presupuesto ajustado de JavaScript y brindar valor en la menor cantidad posible 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 si entregas secuencias de comandos y datos críticos con <link rel=preload>. También te recomendamos que consideres usar patrones como PRPL para asegurarte de que las navegaciones iniciales y posteriores se sientan instantáneas.

Diagrama que muestra la renderización del cliente que afecta el FCP y el TTI.
FCP y TTI con renderización del cliente.

La principal desventaja de la renderización del cliente es que la cantidad de JavaScript requerida 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 adición de nuevas bibliotecas de JavaScript, polyfills y código de terceros, que compiten por la potencia 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 deberían considerar la división de código agresiva para reducir la TBT y la INP durante la carga de la página, así como la carga diferida de JavaScript para entregar solo lo que el usuario necesita, cuando lo necesita. Para 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 te permite aplicar la técnica de caché de la shell de la aplicación. En combinación con los trabajadores del servicio, esto puede mejorar de forma significativa el rendimiento percibido en las visitas repetidas, ya que la página puede cargar su shell de aplicación HTML y las dependencias de CacheStorage con mucha rapidez.

La rehidratación combina el procesamiento del servidor y del cliente.

La rehidratación es un enfoque que intenta suavizar las compensaciones entre el procesamiento del cliente y el del servidor realizando ambos. Las solicitudes de navegación, como las cargas o las cargas de página completas, las controla un servidor que renderiza la aplicación en HTML y, luego, el código JavaScript y los datos que se usan para la renderización se incorporan en el documento resultante. Cuando se realiza con cuidado, se logra un FCP rápido, como la renderización del servidor, y luego se "recupera" volviendo a renderizar en el cliente. Esta es una solución eficaz, pero puede tener inconvenientes de rendimiento considerables.

La principal desventaja de la renderización del servidor con rehidratación es que puede tener un impacto negativo significativo en la TBT y la INP, incluso si mejora la FCP. Las páginas renderizadas del servidor pueden parecer cargadas e interactivas, pero en realidad no pueden responder a las entradas 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 puede confundir y frustrar al usuario.

Un problema de hidratación: una app por el precio de dos

Para que el código JavaScript del cliente "reanude" con precisión desde donde quedó 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 mucho HTML, la rehidratación puede causar más problemas que solo la interactividad retrasada.

Documento HTML que contiene una IU serializada, datos intercalados y una secuencia de comandos bundle.js
Código duplicado en el documento HTML.

El servidor muestra una descripción de la IU de la aplicación en respuesta a una solicitud de navegación, pero también muestra los datos de origen que se usan para componer esa IU y una copia completa de la implementación de la IU que 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 y la rehidratación del servidor 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 estar lista, pero ninguna de sus funciones interactivas funciona.

Diagrama que muestra la renderización del cliente que afecta negativamente el TTI.
Los efectos de la renderización del cliente en el TTI.

Sin embargo, hay esperanza para la renderización del servidor con la rehidratación. A corto plazo, usar solo la renderización del servidor para el contenido altamente almacenable en caché puede reducir el tiempo de respuesta del servidor, lo que produce resultados similares a la renderización previa. La rehidratación gradual, progresiva o parcial podría ser la clave para que esta técnica sea más viable en el futuro.

Transmite el procesamiento del servidor y vuelve a hidratar de forma progresiva

La renderización del servidor tuvo varios desarrollos en los últimos años.

La renderización del servidor en tiempo real te permite enviar HTML en fragmentos que el navegador puede renderizar de forma progresiva a medida que los recibe. Esto puede enviar el marcado a los usuarios más rápido, lo que acelera el FCP. En React, las transmisiones son asíncronas en renderToPipeableStream(), en comparación con renderToString() síncrona, lo que significa que la contrapresión se maneja bien.

También vale la pena considerar la rehidratación progresiva, que React 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 a la vez. Esto puede ayudar a reducir la cantidad de JavaScript necesaria para hacer 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 las inicia.

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: un árbol DOM renderizado por el servidor se destruye y, luego, se vuelve a compilar de inmediato, por lo general, porque la renderización síncrona inicial del cliente requería datos que aún no estaban listos, a menudo un Promise que aún no se resolvió.

Rehidratación parcial

La rehidratación parcial resultó difícil de implementar. Este enfoque es una extensión de la rehidratación progresiva que analiza elementos individuales de la página (componentes, vistas o árboles) y los identifica con poca interactividad o sin reactividad. Para cada una de estas partes, en su mayoría estáticas, el código JavaScript correspondiente se transforma en referencias inertes y funciones decorativas, lo que reduce su espacio en el cliente a casi cero.

El enfoque de hidratación parcial tiene sus propios problemas y compromisos. 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 completa de la página.

Renderización trismórfica

Si los servicios en primer plano son una opción para ti, considera la renderización trismórfica. Es una técnica que te permite usar la renderización del servidor de transmisión para las navegaciones iniciales o no de JS y, luego, hacer que tu trabajador del servicio se encargue de la renderización de 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 enrutamiento entre el servidor, la página del cliente y el trabajador de servicio.

Renderización trismórfica, que muestra un navegador y un trabajador de servicio que se comunican con el servidor
Diagrama de cómo funciona la renderización trismórfica.

Consideraciones de SEO

Cuando eligen una estrategia de renderización web, los equipos suelen tener en cuenta el impacto del SEO. La renderización del servidor es una opción popular para ofrecer una experiencia "completa" que los rastreadores puedan interpretar. Los rastreadores pueden entender JavaScript, pero a menudo hay limitaciones en la forma en que se renderizan. La renderización del cliente puede funcionar, pero a menudo requiere 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.

Cuando tengas dudas, la herramienta de prueba de optimización para dispositivos móviles es una excelente manera de comprobar que el enfoque que elegiste 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 cualquier error que se encuentre durante la renderización.

IU de la prueba de optimización para dispositivos móviles.
IU de la prueba de optimización para dispositivos móviles.

Conclusión

Cuando decidas qué enfoque usar para la 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ía de los objetivos. Está bien enviar principalmente HTML con JavaScript mínimo para obtener una experiencia interactiva. Esta es una infografía útil que muestra el espectro de servidor-cliente:

Inforgrafía que muestra el espectro de opciones que se describen en este artículo.
Opciones de renderización y sus ventajas y desventajas.

Créditos

Gracias a todos por sus comentarios y su inspiración:

Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson y Sebastian Markbåge