wandres.dev
RENDIMIENTO EN MÓVIL · El dispositivo real de tus usuarios

El thermal throttling: el rendimiento que se degrada con el tiempo

Por qué un teléfono solo sostiene su velocidad máxima durante segundos, cuánto pierde en una sesión larga, cómo detectar la degradación sin API térmica, y qué patrones de código la provocan.

⏱ 18 min

Todas las mediciones de este track asumen implícitamente que el dispositivo tiene una velocidad. Un teléfono no la tiene: tiene una velocidad máxima que sostiene unas decenas de segundos y una velocidad sostenida bastante menor a la que acaba cayendo. Eso significa que tu página puede ir bien en la prueba de treinta segundos y mal en el minuto ocho de una sesión real, y que dos ejecuciones consecutivas de la misma medida en el mismo aparato no son comparables entre sí.

🎯 Al terminar esta lección sabrás
  • Explicar por qué un dispositivo sin refrigeración activa no puede sostener su frecuencia máxima.
  • Cuantificar la caída de rendimiento sostenido y su curva temporal.
  • Detectar la degradación con un medidor propio, sin API de estado térmico.
  • Reducir el trabajo sostenido con los patrones que atacan la causa.

Por qué ocurre

Un procesador convierte energía en calor, y la cantidad de calor crece más deprisa que la frecuencia. Un portátil disipa ese calor con ventiladores y masa metálica; un teléfono no tiene ni una cosa ni la otra, y su único mecanismo de refrigeración es la superficie de su propia carcasa, que además está pegada a una mano.

La consecuencia es que la frecuencia máxima que anuncia la ficha técnica es una frecuencia de ráfaga: se puede sostener mientras el aparato esté frío, y en cuanto un sensor interno detecta que la temperatura sube, el sistema baja la frecuencia. Si la carga continúa, sigue bajando, y puede llegar a apagar los núcleos rápidos y dejar el trabajo en los eficientes.

El perfil temporal típico bajo carga sostenida:

Momento Rendimiento relativo al pico
0 a 30 segundos 100 %
30 a 120 segundos 80 a 95 %
2 a 5 minutos 60 a 80 %
Más de 10 minutos 45 a 70 %

Los rangos son amplios a propósito: dependen del modelo, de la temperatura ambiente, de si está cargando y de si tiene funda. Lo que no depende de nada es la forma de la curva: siempre baja, y baja bastante. Un teléfono al que le pides trabajo continuo acaba, al cabo de diez minutos, a una fracción notable de la velocidad con la que empezó.

Hay tres factores que empeoran la curva y conviene conocerlos porque se dan a la vez en el uso real. Cargar calienta, así que un teléfono enchufado se degrada antes; es justo la configuración en la que muchos laboratorios ejecutan sus pruebas, y sesga los resultados hacia abajo. El brillo alto de la pantalla calienta, y una sesión al aire libre lleva el brillo al máximo. Y el modo de bajo consumo, cuando el usuario lo activa o el sistema lo activa solo con la batería baja, reduce las frecuencias de forma deliberada, con lo que se suma un segundo mecanismo de degradación al térmico. Una parte no despreciable de tus usuarios está en ese modo en cualquier momento dado.

No hay ninguna API web que informe del estado térmico. No existe, y no parece que vaya a existir por motivos de huella digital identificable. Así que la única forma de saber si está ocurriendo es inferirlo midiendo.

Detectarlo con un medidor propio

La técnica es sencilla: ejecutar periódicamente un trabajo de referencia idéntico y ver si su duración crece. Si el mismo cálculo tarda cada vez más, el dispositivo se ha degradado.

// Sonda de degradacion. Trabajo pequeno y fijo, cada 30 segundos,
// solo con la pagina visible y en tiempo libre.
const TRABAJO = 40000;

function pulso() {
  const t = performance.now();
  let s = 0;
  for (let i = 0; i < TRABAJO; i++) s += Math.sqrt(i) * 1.0000001;
  return { ms: performance.now() - t, s };
}

export function vigilarDegradacion(alCambiar) {
  let referencia = null;
  let ultimo = null;

  const tomar = () => {
    if (document.visibilityState !== 'visible') return;
    const { ms } = pulso();
    if (referencia === null) {
      referencia = ms;
      return;
    }
    const factor = ms / referencia;
    // Cambio significativo respecto a la ultima notificacion.
    if (ultimo === null || Math.abs(factor - ultimo) > 0.15) {
      ultimo = factor;
      alCambiar({ factor: Number(factor.toFixed(2)), msActual: Math.round(ms) });
    }
  };

  const programar = () => {
    if ('requestIdleCallback' in window) requestIdleCallback(tomar, { timeout: 2000 });
    else tomar();
  };

  programar();
  return setInterval(programar, 30000);
}
vigilarDegradacion(({ factor }) => {
  if (factor > 1.6) {
    // El dispositivo va a un 60 % o menos de lo que iba. Aligera.
    document.documentElement.classList.add('modo-ligero');
  } else if (factor < 1.25) {
    document.documentElement.classList.remove('modo-ligero');
  }
});

Cuatro cosas de este código importan. El trabajo es pequeño: cuarenta mil iteraciones son un puñado de milisegundos incluso en un dispositivo lento, así que la sonda no contribuye al problema que mide. Se ejecuta solo con la página visible, porque en segundo plano los temporizadores se limitan y el resultado no significaría nada. Se ejecuta en tiempo libre para no competir con una interacción. Y la primera medida se toma como referencia en lugar de usar un valor absoluto, con lo que el factor es relativo a ese mismo dispositivo y no hace falta calibrar nada.

El uso más valioso de esta sonda no es reaccionar sino medir: enviar el factor junto a los datos de campo permite comprobar una hipótesis que casi nadie ha verificado en su producto, que es si el INP se degrada con la duración de la sesión. Correlacionar INP con minutos transcurridos, segmentado por clase de dispositivo, produce a menudo un resultado incómodo y accionable.

Y una advertencia sobre reaccionar automáticamente: cambiar la interfaz porque el dispositivo se ha calentado tiene que ser reversible y poco llamativo. Reducir la tasa de refresco de una animación, dejar de precargar, aumentar el intervalo de un sondeo: todo eso es invisible y correcto. Cambiar el diseño o esconder funcionalidad produce una experiencia desconcertante, porque el usuario no tiene forma de saber por qué la aplicación se ha vuelto otra.

Qué provoca la degradación

El calor lo genera el trabajo sostenido, así que el objetivo es reducir el trabajo continuo, no el trabajo puntual. Los cuatro sospechosos habituales, en orden de frecuencia:

Bucles de animación que no paran. Un requestAnimationFrame que se vuelve a programar siempre, aunque no haya nada que animar y aunque el elemento esté fuera de la pantalla, mantiene el dispositivo trabajando a sesenta fotogramas por segundo indefinidamente. Es la causa más común y la más fácil de arreglar:

// Solo anima lo que se ve, y para del todo cuando no hay nada visible.
const visibles = new Set();

const observador = new IntersectionObserver((entradas) => {
  for (const e of entradas) {
    if (e.isIntersecting) visibles.add(e.target);
    else visibles.delete(e.target);
  }
  if (visibles.size > 0) arrancar();
});

let id = null;
function arrancar() {
  if (id !== null) return;
  const paso = () => {
    if (visibles.size === 0 || document.visibilityState !== 'visible') {
      id = null;
      return;   // Se para de verdad, no se sigue programando.
    }
    for (const el of visibles) dibujar(el);
    id = requestAnimationFrame(paso);
  };
  id = requestAnimationFrame(paso);
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible' && visibles.size > 0) arrancar();
});

Sondeos periódicos. Un setInterval que pregunta al servidor cada dos segundos mantiene despierta la radio del móvil, que es de los componentes que más energía consumen. Aquí la mejora no es solo térmica: es de batería, y es de las que el usuario percibe. Un canal de eventos del servidor o una conexión persistente consumen mucho menos que un sondeo frecuente, y si hay que sondear, el intervalo debe crecer con el tiempo de inactividad.

Observadores caros que se disparan mucho. Un observador de mutaciones sobre todo el documento con subárbol incluido se ejecuta en cada cambio del DOM. Un manejador de scroll sin agrupar en fotogramas se ejecuta decenas de veces por segundo. Los dos convierten una interacción normal en trabajo continuo.

Trabajo fuera de la pantalla. Elementos que se siguen calculando, pintando o animando cuando no están visibles. Es exactamente el problema que resuelve el aislamiento de content-visibility, y aquí tiene una segunda justificación además de la del pintado: el trabajo que no se hace no calienta.

El rendimiento sostenido es una métrica distinta del rendimiento de carga, y casi nadie la mide, lo que explica por qué tantas aplicaciones son rápidas al abrir y desesperantes al cabo de un rato

Fíjate en un sesgo estructural de todo el ecosistema de herramientas de rendimiento web: prácticamente todas miden los primeros segundos. La auditoría automática carga la página, espera a que se estabilice y termina. Las métricas de carga se disparan una vez y se acabó. Los perfiles de rendimiento se graban durante unos segundos porque grabar más produce un fichero inmanejable. Incluso las métricas de interacción, que sí se recogen durante toda la visita, se reportan como un único valor agregado que no distingue el minuto uno del minuto veinte. El resultado es un ecosistema entero que optimiza magníficamente los primeros diez segundos de una sesión y es ciego a los siguientes cincuenta minutos. Y eso importa muchísimo más de lo que la atención relativa sugiere, porque la distribución de la atención del usuario es la contraria: si tu producto se usa de verdad, la inmensa mayoría de los minutos que pasa alguien contigo son minutos tardíos, no el arranque. Un panel de trabajo, un editor, un mapa, una aplicación de mensajería, un juego: el arranque ocurre una vez y la sesión dura media hora. Hay tres fenómenos que solo aparecen en esa mitad no medida y los tres son acumulativos, que es lo que los hace peligrosos: la degradación térmica de esta lección, el crecimiento del heap y la presión sobre el recolector, y el crecimiento del DOM y de los escuchadores en aplicaciones que nunca recargan. Ninguno de los tres se nota en una prueba de treinta segundos y los tres empeoran de forma monótona. La instrumentación que corrige la ceguera es sorprendentemente barata y casi nadie la tiene: adjunta a cada informe de interacción el tiempo transcurrido desde el inicio de la sesión, y mira tus percentiles por tramos de cinco minutos. Es una columna más en tu recogida de campo y una dimensión más en el panel. Si la curva es plana, enhorabuena y ya lo sabes con datos. Si sube, acabas de encontrar un problema que llevaba años ahí y que ninguna herramienta te iba a enseñar, porque ninguna estaba mirando. Y el arreglo, cuando lo hay, suele ser de los baratos: un bucle que no paraba, un sondeo demasiado frecuente, un array que crecía sin tope. Lo difícil no era arreglarlo; era saber que estaba pasando.

⚔️ Mide la degradación
  1. Ejecuta la página de calibración de la lección anterior en un teléfono real diez veces seguidas sin pausa y dibuja la curva. Anota la caída porcentual.
  2. Repite el experimento con el teléfono enchufado y compara con la ejecución con batería.
  3. Despliega la sonda de degradación y correlaciona el factor con los minutos de sesión en tus datos de campo.
  4. Añade el tiempo de sesión a cada informe de interacción y mira tu percentil 75 de INP por tramos de cinco minutos.
  5. Busca en tu código todos los requestAnimationFrame que se reprograman incondicionalmente y ponles una condición de parada real.