Lección 09

¿Dónde se construye la página: en el servidor o en tu navegador?

La misma pantalla se puede armar en el servidor, en tu navegador o antes de que nadie la pida. Cada opción te cobra en una moneda distinta: segundos en blanco, JavaScript de más o datos viejos.

01Qué es cada forma de entregar una página

Piensa en un departamento. Puedes comprarlo en obra gris y amueblarlo tú, recibirlo amueblado con la luz sin conectar, o rentarlo de catálogo. La sala se ve igual en los tres casos; cambia cuándo puedes sentarte y cuánto trabajo te toca.

El renderizado en el cliente (CSR, client-side rendering) es la obra gris: el servidor manda un HTML casi vacío y un archivo de JavaScript, el lenguaje con el que el navegador modifica la página después de cargarla. El navegador lo descarga, lo ejecuta y solo entonces construye el árbol de elementos que pinta. Hasta ese momento la pestaña está en blanco, aunque la red ya haya terminado.

El renderizado en el servidor (SSR) es el departamento amueblado: el servidor arma el HTML con los datos dentro y lo manda completo, así que el navegador pinta texto y cajas antes de entender una línea de JavaScript. Falta el enchufe: los botones se ven pero no responden hasta que el JavaScript llega y se ejecuta.

El sitio generado en el build (SSG, static site generation) es el de catálogo: ese HTML se produjo antes de publicar, en el build, el paso previo donde se preparan los archivos sin que nadie lo pida en vivo. Es lo más barato de servir y lo más viejo posible.

La revalidación incremental (ISR) parte de ese archivo y lo regenera cada cierto tiempo, o cuando alguien lo pide después de vencer. Mientras se regenera sigue sirviendo la copia anterior: eso es stale-while-revalidate, servir lo viejo mientras se cocina lo nuevo. Ganas frescura sin una consulta por visita, pero dos personas pueden ver precios distintos durante segundos.

El streaming no cambia dónde se arma la página: cambia cuándo te la mandan. El servidor empieza a escribir la respuesta con la cabecera y el armazón, y sigue escribiendo después; el navegador pinta cada pedazo cuando llega. Si una sección tarda en tener datos, se deja un hueco y se rellena al final.

Streaming: la página llega en tres trozos 1 · cabecera 2 · lista 3 · pie y botón se puede leer se puede desplazar ya responde primer pintado: aquí la página ya sirve para algo
Tres momentos del mismo documento: lo verde acaba de llegar. El primer pintado es el primer trozo; solo el botón espera al tercero.

La hidratación es el momento en que el JavaScript recorre el HTML ya pintado, reconoce cada pieza y le enchufa los eventos: el clic, el teclado, el foco. El HTML estático no sabe nada de ti; la hidratación le enseña a reaccionar. Y aquí aparece el problema: el servidor pintó un árbol y el cliente calcula otro. Cuando no coinciden, el marco de trabajo no puede saber cuál es el bueno y tira el del servidor para rehacerlo. Eso es el parpadeo: el texto cambia, el botón se mueve, el foco se pierde.

Los componentes de servidor (RSC, React Server Components) llevan la idea más lejos: se ejecutan solo en el servidor y no mandan JavaScript. Lo que viaja es una descripción de lo ya armado, no el código que lo armó. La frontera se marca con la directiva 'use client': de ese lado sí viaja todo. Uno de servidor puede leer la base y formatear un precio; uno de cliente puede tener estado y escuchar clics. La pregunta no es cuál es mejor, sino dónde empieza la interacción.

CSR pinta tarde; SSR pinta pronto e hidrata después CSR bajar JS ejecutar JS pintar se ve nada que pintar: la pestaña sigue en blanco (~2 s) SSR HTML listo pinta se ve baja JS hidrata responde se ve, pero los clics todavía no hacen nada 0 ~2 s ~4 s
Las dos filas comparten la escala. La barra roja marca el intervalo en que la página no sirve: en CSR es la pantalla en blanco; en SSR es el rato en que se ve pero no responde.
La frontera de servidor y cliente se traza donde empieza la interacción servidor: datos y texto cliente: interacción consulta la base formatea el precio arma la vista sin JS no hidrata 0 KB 'use client' BotonComprar clic y estado misma pantalla el botón responde el texto no viaja como JavaScript; el botón sí, porque tiene interacción
El corte no es simétrico: lo de servidor se queda como vista armada y no pesa en el navegador; lo de cliente se descarga para poder responder a un clic.

02Por qué sucede

Con SSR el trabajo se paga dos veces: el servidor pinta un árbol y el cliente vuelve a calcular el mismo para poder actualizarlo. Si no coinciden, gana el segundo. Las discrepancias vienen de lo que depende del momento o del aparato: Date.now(), Math.random(), localStorage, el ancho de la ventana, la zona horaria. El servidor no las conoce igual, y el HTML describe algo que ya no es cierto cuando el JavaScript lo revisa.

De ahí la confusión más común: diferir no es reducir. Un import() dinámico (cargar un módulo solo cuando se pide) mueve el costo de lugar, no lo borra: la librería de gráficas de 90 KB sigue pesando 90 KB cuando abres el panel, y ahora hay una espera nueva. Reducir es sacarla del grafo de dependencias: un SVG estático, las funciones del navegador o 40 líneas propias.

El criterio

La pregunta que decide casi todo: cuánto JavaScript manda tu primera pantalla y qué necesita ser interactivo de inmediato. Un párrafo no necesita JavaScript; un botón de compra sí; un acordeón que nadie abrió, no.

03Cómo se ataca

Decide por superficie, no por moda. Un catálogo que cambia cada hora es candidato a SSG con revalidación; un panel con datos del usuario pide SSR o CSR detrás del inicio de sesión; una página de marketing no necesita hidratación. Cuando se mezclan, el patrón de islas deja el HTML estático y vuelve interactivos solo los huecos que lo requieren.

RSC permite partir en el lugar exacto: el componente de servidor lee los datos y el de cliente maneja el clic; solo el segundo se descarga. Después se pone un presupuesto de bundle (el paquete de archivos que se descarga): un número en kilobytes comprimidos que el build hace cumplir y que falla la integración si se pasa. Para la ruta de inicio, 170 KB comprimidos es un punto de partida; importa menos el número que el hecho de que exista y se mida por ruta.

// app/producto/[id]/page.jsx — corre solo en el servidor
// Ni la consulta ni el formateo de precios viajan al navegador.
import { getProducto } from '@/lib/productos';
import BotonComprar from './BotonComprar'; // el único que sí viaja

export default async function Pagina({ params }) {
  const producto = await getProducto(params.id); // datos aquí
  return (
    <article>
      <h1>{producto.nombre}</h1>
      <p>{formatoPrecio(producto.precio)}</p>
      {/* La frontera va donde empieza el clic: el título
            es texto; el botón necesita estado, así que solo
            el botón es de cliente. */}
      <BotonComprar id={producto.id} agotado={producto.stock === 0} />
    </article>
  );
}

// app/producto/[id]/BotonComprar.jsx
'use client'; // de aquí para abajo, este código se descarga e hidrata
import { useState } from 'react';

export default function BotonComprar({ id, agotado }) {
  const [enviando, setEnviando] = useState(false);
  if (agotado) return <button disabled>Agotado</button>;
  return (
    <button disabled={enviando} onClick={() => setEnviando(true)}>
      {enviando ? 'Agregando...' : 'Agregar'}
    </button>
  );
}

El comentario del código es la decisión completa: el corte se hace donde cambia la naturaleza del trabajo, no donde queda simétrico. Lo del servidor deja de ser peso descargable; lo que cruza al cliente se justifica con una interacción concreta.

04Cómo diagnosticarlo

El síntoma siempre es el mismo: algo tarda en aparecer o en responder. La herramienta dice cuál de las dos cosas es.

HerramientaQué se miraCómo se ve el síntoma
DevTools › Lighthouse › LCP El Largest Contentful Paint: cuándo aparece el elemento más grande de la primera pantalla y qué lo retrasa. Si el LCP cae después de la hidratación, el HTML no traía el contenido: CSR puro o SSR esperando datos.
DevTools › Network El orden de los recursos y la columna Initiator: qué import arrastró qué. Un HTML de 2 KB seguido de 400 KB de JavaScript es la firma de CSR; varios JS encadenados son una cascada.
DevTools › Coverage Qué porcentaje del código descargado se ejecutó de verdad en esa ruta. Entre 60% y 80% sin usar: mandas la aplicación entera para pintar una vista.
Lighthouse › Reduce unused JavaScript Cuántos KB se pueden quitar y de qué archivos, con el ahorro estimado. Un archivo grande con poco uso es candidato a diferir; diez pequeños con poco uso, a borrar.
La prueba que conviene hacer

Abre la página con 4G simulado y JavaScript deshabilitado: lo que se ve y no se puede usar depende de la hidratación.

Para decirlo en la entrevista

“Server-rendered HTML paints before any JavaScript runs.”

“Hydration is a second render, so a mismatch throws away the server tree.”

“The budget is per route: what does this screen need before it is useful?”

Comprueba que lo tienes

Cuatro preguntas. Al comprobar se marca la lección como completada.

El HTML llega con todo el contenido y se pinta rápido, pero el botón de agregar no responde durante dos segundos. ¿Cuál es la causa más probable?

El servidor pinta “Hace 2 minutos” y el cliente calcula “Hace 3 minutos”. El marco de trabajo rehace esa parte del árbol. ¿Qué estás viendo?

Mueves una librería de gráficas a un import() dinámico que solo se carga al abrir un panel. ¿Qué lograste?

Un catálogo de 4.000 productos se genera en el build. Cambias el precio de uno y no aparece hasta el siguiente despliegue. ¿Qué ataca esto sin volver a SSR puro?