wandres.dev
PERFORMANCE III · Diagnóstico por síntoma

El perfil de la hidratación: la página que se ve y no responde

La silueta del bloque continuo de arranque, cómo distinguir hidratación de otro trabajo de inicio, y las tres mediciones que separan lo que se ve de lo que funciona.

⏱ 18 min

Hay un síntoma que las métricas de carga puntúan como excelente y los usuarios describen como roto: la página aparece completa y bonita casi al instante, y durante los siguientes segundos no responde a nada. Los clics se pierden, los campos no aceptan texto, los menús no se abren. En el perfil, ese intervalo tiene una forma inconfundible: un bloque continuo y masivo de ejecución de JavaScript que empieza cuando llega el paquete principal y no suelta el hilo hasta que termina.

🎯 Al terminar esta lección sabrás
  • Reconocer el bloque de arranque en el perfil y medir su duración exacta.
  • Distinguir la hidratación de otro trabajo de inicialización que produce la misma silueta.
  • Medir el hueco entre el instante en que la página se ve y aquel en que responde.
  • Enumerar las estrategias que reducen el bloque y qué compromete cada una.

La silueta

Tres rasgos la definen.

Un bloque de script sin interrupciones, de cientos de milisegundos o de varios segundos, que empieza inmediatamente después de que el paquete principal termine de descargarse. En el gráfico de llamas, una pila profunda con funciones de framework repitiéndose una y otra vez a distintas profundidades.

Fotogramas ausentes durante todo el bloque. La pista de fotogramas está vacía o tiene un único fotograma larguísimo. La página no se está repintando porque el hilo no está disponible.

Interacciones con un retraso de entrada enorme, si el usuario intentó hacer algo durante ese intervalo. Es la firma decisiva: el manejador tardó dos milisegundos y la interacción duró mil ochocientos, porque mil setecientos noventa y ocho se fueron esperando a que el hilo se liberase.

Un detalle que ayuda a confirmarlo: en la pista de red, todo está terminado. Los recursos llegaron. El navegador tiene todo lo que necesita y está ocupado ejecutando.

Distinguir hidratación de otros arranques

La misma silueta la producen cuatro cosas distintas, y confundirlas lleva a optimizar la equivocada. La forma de separarlas es mirar qué hay dentro del bloque en el gráfico de llamas.

Hidratación propiamente dicha. El framework recorre el árbol que el servidor envió, crea su representación interna, asocia cada nodo existente con el componente que lo describe y registra los manejadores. Se reconoce porque las funciones repetidas son de reconciliación y de montaje, y porque el número de repeticiones se corresponde con el número de componentes de la página.

Ejecución y evaluación del propio paquete. Antes de hidratar nada hay que ejecutar los módulos: definir clases, evaluar constantes, ejecutar efectos secundarios de nivel superior. Se reconoce porque las funciones tienen nombres de módulo y no se repiten: cada una aparece una vez.

Inicialización de librerías. Un gestor de estado que rehidrata su almacén, un cliente de datos que reconstruye su caché, un sistema de internacionalización que analiza un catálogo de traducciones. Aparece como bloques grandes con nombres de librería.

Trabajo de la aplicación disfrazado de arranque. Cálculos que la aplicación hace al montar: ordenar una lista de cinco mil elementos, construir índices, formatear fechas. Es el más fácil de arreglar de los cuatro, y el que más veces resulta ser el dominante cuando se mira de verdad.

ℹ️
Nota

La distinción práctica más útil: el coste de la hidratación es proporcional al número de nodos del árbol renderizado, mientras que el de la ejecución del paquete es proporcional a su tamaño. Si duplicas el contenido de la página y el bloque crece, es hidratación. Si añades una librería y crece, es evaluación. Es un experimento de cinco minutos que decide en qué dirección invertir semanas.

Medir el hueco entre verse y funcionar

Ninguna métrica estándar mide esto, así que hay que instrumentarlo. La medición correcta tiene tres instantes.

// Mide el hueco entre que la pagina se ve y que responde de verdad
(() => {
  const marca = (nombre) => {
    performance.mark(nombre);
    performance.measure(nombre + ' desde navegacion', { start: 0, end: performance.now() });
  };

  // 1. Cuando se pinto el contenido principal
  new PerformanceObserver(l => {
    const e = l.getEntries().at(-1);
    window.__msVisible = e.startTime;
  }).observe({ type: 'largest-contentful-paint', buffered: true });

  // 2. Cuando el hilo principal deja de estar continuamente ocupado.
  //    Sonda: un temporizador de 50 ms que mide su propio retraso.
  let ultimaOcupacion = 0;
  const sonda = () => {
    const esperado = performance.now() + 50;
    setTimeout(() => {
      const retraso = performance.now() - esperado;
      if (retraso > 50) ultimaOcupacion = performance.now();
      if (performance.now() < 20000) sonda();
    }, 50);
  };
  sonda();

  // 3. Tu propia marca de interactividad, puesta donde la aplicacion lo sabe
  window.marcarInteractiva = () => {
    marca('app:interactiva');
    const visible = Math.round(window.__msVisible ?? 0);
    const interactiva = Math.round(performance.now());
    console.table([{
      seVeMs: visible,
      respondeMs: interactiva,
      huecoMs: interactiva - visible,
      ultimoBloqueoMs: Math.round(ultimaOcupacion)
    }]);
    if (interactiva - visible > 500)
      console.warn('Hueco grande: la pagina parecio lista mucho antes de serlo.');
  };

  console.log('Llama a marcarInteractiva() cuando la aplicacion este realmente lista.');
})();

La sonda del punto dos es la parte más útil y la que menos gente usa: un temporizador corto que mide su propio retraso detecta cualquier bloqueo del hilo principal sin necesidad de ninguna API especializada, funciona en todos los navegadores, y el instante de su último retraso grande es una aproximación excelente al momento en que la aplicación se calmó de verdad.

Las estrategias y su precio

Enviar menos JavaScript. Es la única que ataca la causa. Auditar el paquete de arranque componente a componente, sacar del camino crítico todo lo que no sea necesario para la primera pantalla. Cuesta trabajo continuo y no tiene contrapartida técnica.

Hidratar solo lo que hace falta. Que el contenido puramente estático no se hidrate en absoluto y que los componentes interactivos se hidraten por separado, cada uno cuando corresponde. Es la que produce los saltos más grandes en aplicaciones con mucho contenido, y su precio es arquitectónico: obliga a distinguir explícitamente entre lo interactivo y lo que no lo es, cosa que muchos frameworks no facilitan y algunos hacen imposible.

Retrasar la hidratación de lo que está fuera de la pantalla hasta que se acerque al área visible o hasta que el usuario interactúe con ello. Su precio es la complejidad de coordinar el estado entre partes que se despiertan en momentos distintos.

Trocear la hidratación cediendo el control entre componentes. No reduce el trabajo total, y de hecho lo aumenta ligeramente, pero elimina el bloqueo continuo y permite que las interacciones se atiendan mientras tanto. Es la mejora más barata de implementar y la que menos mejora los números de laboratorio, aunque mejore mucho la experiencia real.

No hidratar nada. Que el servidor envíe el estado necesario para reanudar la ejecución en lugar de reconstruirla. Es la propuesta más ambiciosa y la que impone más restricciones sobre cómo se escribe el código.

Qué comprobar antes de rediseñar

Antes de acometer un cambio arquitectónico, tres comprobaciones que a veces revelan que el problema es mucho más pequeño de lo que parece.

¿Cuántos nodos tiene la página? Un árbol de veinte mil nodos hidrata lento por muchos trucos que se apliquen. Reducir la página a lo que el usuario realmente ve puede resolverlo sin tocar la estrategia de hidratación.

¿Cuánto del bloque es hidratación de verdad? Si al ampliar el gráfico resulta que el sesenta por ciento del tiempo se lo lleva una librería inicializándose o un cálculo de la aplicación, la respuesta no está en la hidratación.

¿Se hidrata algo dos veces? Ocurre más de lo que parece, con componentes que se montan, se desmontan y se vuelven a montar durante el arranque por un cambio de estado temprano. En el gráfico se ve como un patrón que se repite exactamente igual dos veces seguidas, y es un bug, no un coste inherente.

La hidratación es el precio de renderizar dos veces lo mismo, y ese precio se paga en el peor momento posible

Conviene tener claro el origen del coste para juzgar bien las soluciones, porque casi todas las discusiones sobre este tema mezclan síntoma y causa. La hidratación existe porque el renderizado en servidor produce HTML sin comportamiento: el navegador recibe la apariencia final de la interfaz pero no sabe qué componente la generó, qué estado tenía ni qué manejadores le corresponden. Para que la página funcione, el cliente tiene que volver a ejecutar la misma lógica de render que ya ejecutó el servidor, no para producir HTML —ese ya está— sino para reconstruir la estructura de datos que permite actualizarlo después. Es decir: se hace el mismo trabajo dos veces, y la segunda se hace en el dispositivo más lento de los dos, con la CPU menos capaz, en el instante en que el usuario está mirando. Esa formulación explica por qué las soluciones que funcionan de verdad son todas variaciones de la misma idea: hacer menos veces el trabajo duplicado. Las islas lo consiguen hidratando solo los trozos que necesitan comportamiento. La hidratación perezosa lo consigue aplazando los trozos que nadie está mirando. La resumibilidad lo consigue serializando el estado necesario para que el cliente no tenga que reconstruirlo ejecutando nada. Y las tres pagan el mismo precio: obligan a decidir explícitamente qué partes de la interfaz son interactivas, decisión que el modelo de “todo es un componente y todos se comportan igual” evitaba deliberadamente porque simplifica el trabajo del desarrollador. Ahí está el verdadero compromiso, y merece la pena nombrarlo sin adornos: la hidratación completa compra homogeneidad conceptual —un solo modelo mental, un solo tipo de componente, nada que decidir— y la paga con un bloque de trabajo proporcional al tamaño de la página, ejecutado en el peor momento, en el peor dispositivo. Para una página pequeña esa compra es excelente. Para una página grande de contenido, es de las peores decisiones de arquitectura disponibles, y la señal de que se ha tomado mal es exactamente esta silueta en el perfil.