El árbol de decisión por síntoma
La tabla de referencia que va de la frase que dice el usuario a la métrica que la captura, el panel que la revela, las causas en orden de frecuencia y la lección exacta donde está el remedio.
Nadie abre una incidencia diciendo que su percentil 75 de pintado del elemento más grande está en 4,2 segundos. Dice que la página va lenta, que al pulsar no pasa nada, que se le mueve el botón, que el scroll da tirones o que después de un rato el móvil se calienta. Esta lección es el traductor: cinco frases de entrada, y para cada una la métrica que la mide, el panel donde se ve, las causas en el orden en que aparecen y el enlace directo a donde está el remedio. Es la lección que hay que tener abierta en la otra pestaña mientras se depura.
- Traducir cualquier queja de usuario a una métrica concreta y a un panel concreto.
- Recorrer el árbol hasta una causa candidata sin pasar por las cuatro ramas equivocadas.
- Descartar la mitad del árbol en treinta segundos con una sola medida.
- Saltar directamente a la lección del track que contiene el remedio.
La tabla de primer corte
Cinco síntomas cubren la práctica totalidad de lo que se reporta. La columna que más se salta y más importa es la segunda: qué momento de la interacción describe la queja, porque es la que decide la rama y las cinco ramas no comparten ni una sola herramienta.
| Lo que dice el usuario | Momento | Métrica | Dónde se ve | Umbral malo |
|---|---|---|---|---|
| “Tarda en aparecer”, “se queda en blanco” | Carga inicial | LCP y su desglose | Panel de rendimiento, campo con atribución | LCP > 4 s |
| “Pulso y no pasa nada”, “se queda pillado” | Tras una acción | INP y sus tres partes | Métricas en vivo, event timing |
INP > 500 ms |
| “Se me mueve”, “he pulsado donde no era” | Durante la carga | CLS con atribución | Capa de cambios de diseño | CLS > 0,25 |
| “Va a tirones”, “se arrastra al bajar” | Scroll o animación | Fotogramas largos, no hay Core Web Vital | Panel de rendimiento, long-animation-frame |
Fotogramas > 50 ms |
| “Al rato se atasca”, “hay que recargar” | Minutos de uso | Tamaño del heap y nodos vivos | Panel de memoria, tres capturas | Crecimiento monótono |
Y el complemento imprescindible de esa tabla, porque es lo que convierte cada fila en una tarea: las causas de cada síntoma en el orden en el que aparecen, y la lección donde está el remedio.
| Síntoma | Causa 1 | Causa 2 | Causa 3 |
|---|---|---|---|
| Tarda en aparecer | Recurso del LCP descubierto tarde | Servidor lento o lejano | Bloqueo de render por CSS y fuente |
| No responde al pulsar | Hilo ocupado antes del evento | Manejador caro | Repintado enorme tras el cambio |
| Se mueve | Imagen o anuncio sin espacio reservado | Intercambio de fuente | Contenido inyectado por encima |
| Va a tirones | Animación de propiedades que fuerzan layout | Demasiados nodos en el árbol | Manejador de scroll que mide |
| Se atasca al rato | Escuchadores nunca retirados | Nodos desprendidos retenidos | Caché en memoria sin tope |
El árbol completo
flowchart TB
U[Lo que dice el usuario] --> Q{En que momento ocurre}
Q -->|Al abrir| A[LCP y sus cuatro sumandos]
Q -->|Al pulsar o escribir| B[INP y sus tres partes]
Q -->|Solo mientras carga| C[CLS con atribucion]
Q -->|Al desplazar o animar| D[Fotogramas largos]
Q -->|Tras varios minutos| E[Heap y nodos vivos]
A --> A1{Que sumando domina}
A1 -->|TTFB| A2[Origen cache en el borde y distancia]
A1 -->|Retraso de carga| A3[Descubrimiento y prioridad del recurso]
A1 -->|Tiempo de carga| A4[Formato tamano y ancho servido]
A1 -->|Retraso de render| A5[CSS bloqueante fuente e hidratacion]
B --> B1{Que parte domina}
B1 -->|Retraso de entrada| B2[Tareas largas terceros e hidratacion]
B1 -->|Procesamiento| B3[Trocear ceder el hilo o mover a un worker]
B1 -->|Presentacion| B4[Layout forzado y DOM demasiado grande]
C --> C1{Que elemento desplaza}
C1 -->|Imagen o video| C2[Reservar espacio con dimensiones]
C1 -->|Texto| C3[Ajustar la fuente de reserva]
C1 -->|Bloque nuevo| C4[Reservar hueco o insertar abajo]
D --> D1[Aislar y componer sin layout]
E --> E1[Tres capturas y buscar el retenedor]
style A fill:#89b4fa,color:#11111b
style B fill:#f9e2af,color:#11111b
style C fill:#cba6f7,color:#11111b
style D fill:#fab387,color:#11111b
style E fill:#f38ba8,color:#11111b
style A3 fill:#a6e3a1,color:#11111b
style B2 fill:#a6e3a1,color:#11111b
style C2 fill:#a6e3a1,color:#11111bLos tres nodos en verde son las hojas más frecuentes de sus respectivas ramas. Si tuvieras que apostar sin mirar nada, apostarías a esas tres, y acertarías más de la mitad de las veces.
Los treinta segundos que descartan cuatro ramas
Antes de abrir ningún panel, pega esto en la consola de la página que se queja, interactúa con ella cinco segundos y mira la salida. No sustituye al diagnóstico, pero dice en qué rama estás, que es lo que decide todo lo demás.
// Triaje de rama. Pegar, interactuar 5 s, leer.
const estado = { lcp: 0, inp: 0, cls: 0, fotogramaPeor: 0 };
new PerformanceObserver((l) => {
const e = l.getEntries().at(-1);
estado.lcp = Math.round(e.startTime);
estado.elementoLcp = e.element?.tagName + (e.url ? ' ' + e.url.slice(-40) : '');
}).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
if (e.interactionId && e.duration > estado.inp) {
estado.inp = Math.round(e.duration);
estado.objetivoInp = e.target?.tagName;
}
}
}).observe({ type: 'event', durationThreshold: 16, buffered: true });
new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
if (!e.hadRecentInput) estado.cls += e.value;
}
}).observe({ type: 'layout-shift', buffered: true });
// Fotogramas largos: la senal del scroll a tirones, que no tiene Core Web Vital.
if (PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
estado.fotogramaPeor = Math.max(estado.fotogramaPeor, Math.round(e.duration));
}
}).observe({ type: 'long-animation-frame', buffered: true });
}
setTimeout(() => {
estado.cls = +estado.cls.toFixed(3);
estado.memoriaMB = performance.memory
? Math.round(performance.memory.usedJSHeapSize / 1048576)
: 'no disponible';
console.table(estado);
}, 5000);
La regla de lectura es mecánica. Un solo número por encima de su umbral y los demás bien: tienes una rama y el resto del árbol sobra. Dos números malos: casi siempre uno es consecuencia del otro, y la relación es predecible —un LCP alto con un INP alto comparte causa en el hilo principal; un CLS alto con un LCP alto suele ser la misma imagen sin dimensiones—. Todos los números bien y el usuario quejándose: no estás reproduciendo su escenario, y el problema está en el segmento, no en la página. Vuelve a segmentar el campo.
Síntoma 1: tarda en aparecer
La métrica es el pintado del elemento más grande, y el diagnóstico no es la métrica sino sus cuatro sumandos. Nunca ataques un LCP sin desglosarlo antes; los cuatro sumandos tienen conjuntos de remedios disjuntos.
| Sumando domina | Qué está pasando | A dónde ir |
|---|---|---|
| Tiempo hasta el primer byte | El servidor tarda o está lejos | Geografía de la latencia, qué cachear en el borde |
| Retraso de carga | El navegador no sabía que necesitaba el recurso | Descubrimiento temprano, el escáner de precarga |
| Tiempo de carga | El recurso pesa demasiado para la red que hay | Elegir formato, srcset y sizes |
| Retraso de renderizado | El recurso está y no se pinta | Eliminar el retraso de renderizado, CSS bloqueante |
Las causas, en el orden en el que se encuentran:
Uno: el recurso del LCP lo descubre JavaScript. La imagen está en un componente que se monta al hidratar, o su URL se compone en tiempo de ejecución, o vive en un background-image de una hoja que llega tarde. El síntoma inequívoco es un retraso de carga de más de un segundo con un tiempo de carga pequeño. Es la causa individual más común de un LCP malo en un sitio moderno, y la más barata de arreglar: quince minutos y una etiqueta. Está resuelta paso a paso en la imagen del hero de principio a fin.
Dos: el documento tarda en llegar. TTFB por encima de 800 milisegundos en el percentil 75. Descompón el propio TTFB: si domina el tiempo de servidor, el problema es tuyo y está en el origen; si domina el establecimiento de la conexión, es distancia y número de orígenes. Y comprueba antes que nada las redirecciones de entrada, que cuestan un viaje completo cada una y se quitan en una línea de configuración: están en el presupuesto de viajes.
Tres: hay bloqueo de renderizado delante. Hojas de estilo en cascada, una fuente sin font-display, un script síncrono en la cabecera. El síntoma es un retraso de renderizado grande con todo lo demás correcto. Empieza por el JavaScript bloqueante y por la cadena de carga de una fuente.
Cuatro: la imagen es enorme para la pantalla que la recibe. Un JPEG de 1,4 MB para un hueco de 380 píxeles CSS. Se ve en un tiempo de carga alto y se arregla con formato y descriptores correctos. Suele valer entre 300 y 900 milisegundos en móvil.
Cinco: la carga diferida está puesta en el elemento del LCP. Sí, sigue pasando, y hoy cuesta entre 200 y 700 milisegundos. Es la primera comprobación de la lista y la que más veces resulta positiva en una auditoría ajena: las trampas de loading=lazy.
Síntoma 2: pulso y no pasa nada
La métrica es la interacción hasta el siguiente pintado, y otra vez lo que resuelve el problema es su descomposición en tres partes. Aquí la descomposición no es una comodidad: es lo único que evita optimizar la parte equivocada, porque si domina el retraso de entrada, tu manejador es irrelevante.
| Parte domina | Significa | A dónde ir |
|---|---|---|
| Retraso de entrada | El hilo estaba ocupado cuando llegó el evento | Reducir el retraso de entrada, encontrar tareas largas |
| Procesamiento | Tus manejadores tardan | Acortar el procesamiento, trocear con cesión |
| Presentación | Pintar el resultado es caro | El retraso de presentación, el layout síncrono forzado |
Las causas, ordenadas:
Uno: la hidratación en curso. El caso clásico del INP malo en los primeros segundos de vida de la página, y el que más contamina el percentil porque afecta a la primera interacción de todo el mundo. Diagnóstico: el INP es horrible en los primeros cinco segundos y aceptable después. Remedio en el impuesto de la hidratación y la hidratación progresiva.
Dos: un script de terceros ocupando el hilo. No lo controlas, y por eso hay que medirlo antes de discutirlo. Medir script a script y, si el coste es grande, el patrón de fachada.
Tres: un manejador que hace todo el trabajo de golpe. Filtrar diez mil elementos en el evento de teclado, recalcular un total en cada pulsación, serializar el estado entero. Se parte en dos: la respuesta visual inmediata y el trabajo pesado después de ceder. Los tres casos canónicos están resueltos en casos típicos de INP.
Cuatro: el cambio es barato de calcular y carísimo de pintar. Añades una clase y el navegador reconstruye el layout de una lista de ocho mil nodos. El procesamiento sale bajo y la presentación alta. Aquí manda el coste de diez mil nodos y, si el número de nodos no baja, content-visibility.
Cinco: hay una lectura forzada dentro del manejador. Un offsetHeight en medio de un bucle de escrituras. El coste es superlineal con el número de elementos y no aparece en ningún perfil como una función cara, sino como tiempo repartido en el recálculo de estilo: detectar el thrashing.
Síntoma 3: se me mueve el contenido
La métrica es el desplazamiento acumulado y el diagnóstico es directo, porque a diferencia de las otras dos la métrica te dice qué elemento ha desplazado. No hay que deducir nada: se atribuye. Empieza siempre por de dónde sale tu CLS.
Uno: una imagen, un vídeo o un anuncio sin espacio reservado. El ochenta por ciento de los casos y el remedio son dos atributos: reservar espacio con aspect-ratio.
Dos: el intercambio de fuente desplaza el texto. Se reconoce porque el desplazamiento ocurre entre 100 y 3.000 milisegundos y afecta a bloques de texto enteros. Se ataca por dos lados: ajustar la fuente de reserva para que ocupe lo mismo, y font-display para elegir qué periodo quieres pagar.
Tres: algo se inyecta por encima de lo que ya estaba. Banner de consentimiento, aviso de envío gratuito, barra de sesión. La regla es que lo que llega tarde no se inserta arriba, o si se inserta se le reserva el hueco desde el HTML: contenido inyectado y animaciones.
Un aviso de diagnóstico que ahorra horas: el CLS de campo y el de laboratorio divergen más que ninguna otra métrica, porque el laboratorio no hace scroll, no acepta consentimientos y no espera a los anuncios. Si el campo dice 0,31 y tu auditoría local dice 0,02, el campo tiene razón y estás mirando el primer segundo de una página que rompe en el quinto. La explicación completa está en por qué campo y laboratorio nunca coinciden.
Síntoma 4: va a tirones
Este síntoma tiene una particularidad que conviene decir en voz alta: no lo captura ninguna Core Web Vital. Un sitio con el scroll a trompicones y un zoom que se atasca puede tener LCP, INP y CLS en verde, porque el INP solo observa clic, toque y pulsación de tecla. Es el mayor punto ciego del cuadro de mando estándar.
Lo que se mide aquí es el presupuesto de fotograma: 16,7 milisegundos a 60 Hz y 8,3 a 120 Hz, y dentro caben el estilo, el layout, el pintado y la composición. La herramienta es el panel de rendimiento leído fotograma a fotograma, más las entradas de fotograma largo en campo.
Uno: se está animando una propiedad que dispara layout. Animar top, left, width o height obliga a recalcular geometría en cada fotograma. La tabla de qué propiedad dispara qué etapa está en qué propiedad dispara qué etapa, y el porqué en el pipeline de píxeles.
Dos: el árbol es demasiado grande para recalcularlo a 60 Hz. El layout es global y no se acota solo: por qué el layout es un problema global. Los dos remedios reales son aislar con contain y no montar lo que no se ve, con virtualización por ventana.
Tres: un manejador de scroll que mide. Cada evento de scroll lee posiciones y escribe estilos, y el patrón correcto es el de dos fases: leer todo y luego escribir todo.
Cuatro: hay demasiadas capas de composición. El remedio para el tirón —promocionar a capa— aplicado sin medir se convierte en la causa del siguiente: capas y composición.
Síntoma 5: al rato se atasca
El síntoma tarda minutos en aparecer, lo que hace que casi nunca se reproduzca en una sesión de depuración de dos minutos. La señal es que el problema empeora con el tiempo de uso y desaparece al recargar, y esa combinación solo la produce la memoria.
Uno: escuchadores que nunca se retiran. Cada vista montada añade uno al objeto global y ninguna lo quita: listeners y cierres.
Dos: nodos desprendidos retenidos por una referencia. Quitar del DOM no libera si alguien sigue apuntando: nodos DOM desprendidos.
Tres: una caché en memoria sin tope. Un Map que acumula respuestas y no expulsa nunca.
La técnica de diagnóstico es siempre la misma y es infalible cuando se hace bien: los tres snapshots. Y si el sitio es de sesiones largas, mídelo también en producción con measureUserAgentSpecificMemory, porque la fuga que importa es la del usuario que tiene la pestaña abierta ocho horas, no la tuya.
Ojo con el falso positivo más habitual de esta rama: en un móvil con poca memoria, la pestaña se descarta y se recarga sola, y el usuario lo describe exactamente igual que una fuga. Distinguirlos es cuestión de mirar si el estado se pierde: memoria limitada y las pestañas.
Los síntomas que no encajan
Cuatro frases frecuentes que no son ninguno de los cinco y que tienen su propio tratamiento.
“A mí me va bien.” No es una rama, es un problema de segmento. La página va bien en tu dispositivo y tu red, y mal en los del usuario. Se resuelve en el dispositivo real de tus usuarios y calibrando la limitación contra un aparato de verdad en throttling de CPU y de red.
“Solo va lento la primera vez.” Diagnóstico casi cerrado: es caché. Mira la política de los estáticos y la validación en immutable y stale-while-revalidate, y recuerda que la segunda visita también se beneficia de la caché de código.
“Va lento al principio de la sesión y luego bien.” Puede ser hidratación, pero si el aparato lleva rato trabajando también puede ser temperatura: el thermal throttling degrada un móvil de forma progresiva y hace que la misma página medida dos veces dé números distintos.
“Iba bien y de repente no.” No es diagnóstico, es regresión. La pregunta no es cuál es la causa sino qué entró: el diferencial del paquete y la bisección sobre la lista de despliegues, en el diff del bundle en el PR. Y si el sitio tiene un service worker, comprueba antes que nada que no está sirviendo una versión rota desde el disco del usuario: el despliegue seguro y la vía de escape.
Hay una manera de leer esta lección que la vuelve inútil, y es tratarla como un diccionario de soluciones: busco mi síntoma, leo la causa uno, la aplico. Funciona a veces, por pura estadística, y cuando funciona enseña lo contrario de lo que hay que aprender. La utilidad real de un árbol de decisión no está en las hojas sino en las ramas que poda, y merece la pena entender por qué eso vale tanto. Un problema de rendimiento tiene, en abstracto, unas cuarenta causas posibles, y esa cifra es lo bastante grande como para que explorarlas en orden arbitrario sea desesperante: cada comprobación cuesta entre diez minutos y media hora, la mayoría dan negativo, y a las tres horas uno ha perdido el hilo de lo que ya descartó. Lo que hace el árbol es convertir cuarenta candidatas en tres o cuatro con dos preguntas, y las dos preguntas son baratísimas: en qué momento de la interacción ocurre, y qué sumando de la métrica domina. Treinta segundos de trabajo que eliminan el noventa por ciento del espacio de búsqueda. Ninguna técnica del track tiene esa relación entre coste y rendimiento. De ahí salen tres consecuencias prácticas que conviene interiorizar. La primera: la pregunta de la rama hay que hacérsela al usuario, no a la herramienta, porque la herramienta te dará números de las cinco ramas y el usuario solo sufre una; “¿ocurre al abrir la página o después de pulsar algo?” es la pregunta más rentable que se puede hacer en una incidencia de rendimiento, y casi nadie la hace. La segunda: cuando dos ramas dan malo a la vez, no tienes dos problemas, tienes uno con dos síntomas, y buscar la causa común es siempre más rápido que atacar las dos por separado; un hilo principal saturado produce a la vez un retraso de renderizado alto, un retraso de entrada alto y un scroll a tirones, y quien no ve la conexión abre tres investigaciones. Y la tercera, la que más cuesta aceptar: cuando todos los números salen bien y el usuario insiste, el usuario tiene razón y tu medición está mal, casi siempre porque estás midiendo otro dispositivo, otra red, otra ruta u otro momento. Ese caso —el que parece que no encaja en el árbol— es en realidad la señal más fiable que existe de que te falta un corte de segmentación, y en cuanto lo encuentras el problema suele estar concentrado, evidente y resuelto en veinte minutos.
- Coge las cinco últimas incidencias de rendimiento que os han llegado y clasifícalas en las cinco ramas usando solo la frase original del usuario.
- Para cada una, anota qué medida habría bastado para confirmar la rama y cuánto habrías tardado en tomarla.
- Ejecuta el guion de triaje en tus tres rutas más visitadas y apunta en qué rama está cada una.
- Comprueba si alguna de tus rutas tiene el punto ciego del síntoma cuatro: métricas en verde y scroll a tirones en un móvil real.
- Escribe tu propia versión de la tabla de primer corte con los nombres de tus rutas y tus paneles, y déjala donde la vea quien recibe las incidencias.