RxJs: Operadores de tiempo

RxJs: Operadores de tiempo

Tabla de Contenido

Cuando un flujo de eventos del navegador emite con demasiada frecuencia, la lógica de aplicación puede terminar procesando ruido en lugar de señales útiles. Los operadores de tiempo RxJS permiten controlar esa presión temporal en escenarios como búsquedas con autocompletado, eventos de scroll, clics repetidos, métricas periódicas o capturas de estado. En este artículo analizaremos debounceTime, throttleTime, sampleTime, sample y auditTime con ejemplos en TypeScript y diagramas de canicas.

Note

Este artículo forma parte de la serie RxJs. Venimos de Operadores básicos y continuaremos con Operadores de transformación. Aquí abordaremos cómo regular emisiones en el tiempo sin acoplar la lógica de negocio a temporizadores manuales.

Por qué usar operadores de tiempo RxJS

En RxJS, un observable puede emitir valores con una cadencia muy superior a la capacidad real de procesamiento de la interfaz o del backend. Un keyup puede dispararse en cada tecla, un mousemove puede emitir decenas de veces por segundo y un botón puede recibir clics duplicados antes de que termine una operación asíncrona. Sin control temporal, el sistema incrementa latencia, consumo de CPU y llamadas innecesarias.

Los operadores de tiempo resuelven este problema dentro del pipeline reactivo. En lugar de mezclar setTimeout, flags mutables y cancelaciones manuales, expresamos la política de emisión con operadores declarativos. Esto mantiene la suscripción enfocada en consumir valores ya estabilizados, muestreados o limitados.

Info

En ReactiveX, estos operadores suelen representarse con diagramas de canicas porque el comportamiento depende tanto del valor como del instante de emisión. Los diagramas del artículo provienen de la nota original y se conservan como referencia visual.

debounceTime: esperar silencio antes de emitir

debounceTime emite el último valor del observable fuente solo cuando transcurre un intervalo sin nuevas emisiones. Si llega otro valor antes de que venza el temporizador, el valor anterior se descarta y el conteo vuelve a empezar. Esta semántica es especialmente útil para entradas de texto, filtros de búsqueda y eventos donde interesa la intención final del usuario, no cada evento intermedio.

Imagen guia del contenido de la página

Consideremos un flujo de clics. La suscripción recibirá un evento únicamente si pasan tres segundos sin otro clic adicional:

import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

const click$ = fromEvent<PointerEvent>(document, 'click');

click$
  .pipe(debounceTime(3000))
  .subscribe(console.log);

En formularios, debounceTime suele combinarse con distinctUntilChanged para evitar trabajo duplicado cuando el valor estabilizado no cambió realmente. La nota original usa pluck('target', 'value'); funciona como referencia histórica, pero pluck está deprecado en RxJS 7. La alternativa moderna es map.

import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';

const input = document.createElement('input');
document.body.appendChild(input);

const input$ = fromEvent<KeyboardEvent>(input, 'keyup');

input$
  .pipe(
    debounceTime(1500),
    map((event) => (event.target as HTMLInputElement).value),
    distinctUntilChanged()
  )
  .subscribe(console.log);

Warning

pluck aparece en muchos ejemplos clásicos de RxJS, pero está deprecado. Para código nuevo, usa map y extrae explícitamente la propiedad requerida; el resultado es más tipado y más claro para TypeScript.

throttleTime: limitar la frecuencia de emisión

throttleTime permite emitir un valor y luego ignorar nuevas emisiones durante una ventana temporal. Por defecto, emite el primer valor de cada ventana y descarta los valores posteriores hasta que el intervalo termina. Es una estrategia útil cuando necesitas una respuesta inmediata, pero quieres proteger el sistema contra ráfagas de eventos.

Imagen guia del contenido de la página

El siguiente ejemplo procesa como máximo un valor de teclado cada 1500 milisegundos:

import { fromEvent } from 'rxjs';
import { distinctUntilChanged, map, throttleTime } from 'rxjs/operators';

const input = document.createElement('input');
document.body.appendChild(input);

const input$ = fromEvent<KeyboardEvent>(input, 'keyup');

input$
  .pipe(
    throttleTime(1500),
    map((event) => (event.target as HTMLInputElement).value),
    distinctUntilChanged()
  )
  .subscribe(console.log);

Configuración leading y trailing

La configuración avanzada de throttleTime permite decidir si se emite el valor inicial de la ventana (leading) y/o el último valor capturado durante esa ventana (trailing). Esta configuración es importante cuando necesitas respuesta inmediata y, además, preservar el estado final de una ráfaga.

import { asyncScheduler, fromEvent } from 'rxjs';
import { distinctUntilChanged, map, throttleTime } from 'rxjs/operators';

const input = document.createElement('input');
document.body.appendChild(input);

const input$ = fromEvent<KeyboardEvent>(input, 'keyup');

input$
  .pipe(
    throttleTime(1500, asyncScheduler, {
      leading: true,
      trailing: true,
    }),
    map((event) => (event.target as HTMLInputElement).value),
    distinctUntilChanged()
  )
  .subscribe(console.log);
Imagen guia del contenido de la página

Con leading: true, la primera emisión sale de inmediato. Con trailing: true, el último valor observado durante la ventana se emite al cerrar el intervalo. Esta combinación es adecuada para interacciones donde importa tanto la reacción inicial como el estado final.

Tip

Usa debounceTime cuando solo importa el valor estabilizado. Usa throttleTime cuando necesitas una primera respuesta rápida y una frecuencia máxima controlada.

sampleTime: tomar snapshots periódicos del último valor

sampleTime toma una muestra del último valor emitido por la fuente cada cierto intervalo. A diferencia de throttleTime, el reloj no se inicia por una emisión del observable fuente; el operador usa un intervalo periódico. Si no hubo valores nuevos desde el último tick, no emite nada.

Imagen guia del contenido de la página

El siguiente pipeline captura la última posición de clic cada dos segundos, siempre que haya ocurrido al menos un clic durante ese periodo:

import { fromEvent } from 'rxjs';
import { map, sampleTime } from 'rxjs/operators';

const click$ = fromEvent<PointerEvent>(document, 'click');

click$
  .pipe(
    sampleTime(2000),
    map(({ x, y }) => ({ x, y }))
  )
  .subscribe(console.log);

Esta estrategia encaja bien en telemetría, dashboards o cálculos de posición donde no necesitas procesar cada evento, sino observar el estado más reciente a una cadencia fija. También reduce el acoplamiento entre la frecuencia de entrada y la frecuencia de procesamiento.

sample: emitir cuando otro observable lo indique

sample generaliza la idea de muestreo: en lugar de usar un intervalo fijo, recibe un observable notificador. Cada vez que el notificador emite, sample publica el último valor conocido de la fuente, si existe uno disponible desde la muestra anterior.

Imagen guia del contenido de la página

En este ejemplo, interval$ genera valores cada 500 milisegundos, pero solo se emite el último valor cuando ocurre un clic:

import { fromEvent, interval } from 'rxjs';
import { sample } from 'rxjs/operators';

const interval$ = interval(500);
const click$ = fromEvent<PointerEvent>(document, 'click');

interval$
  .pipe(sample(click$))
  .subscribe(console.log);

sample es útil cuando un evento externo define el momento de lectura. Por ejemplo, puedes mantener un stream de estado interno y emitir snapshots solo cuando el usuario confirma una acción, cuando llega una señal de sincronización o cuando otro subsistema solicita una captura.

auditTime: emitir el último valor al cerrar una ventana

auditTime inicia una ventana temporal cuando recibe una emisión de la fuente. Durante esa ventana ignora las emisiones intermedias, pero conserva la última. Al finalizar el intervalo, emite ese último valor capturado.

Imagen guia del contenido de la página

Su comportamiento resulta útil cuando el primer valor de una ráfaga no es tan relevante como el valor final dentro de una ventana de procesamiento. El siguiente ejemplo registra cada clic con tap, transforma la posición y luego deja pasar el último valor cada dos segundos desde el inicio de cada ventana:

import { fromEvent } from 'rxjs';
import { auditTime, map, tap } from 'rxjs/operators';

const click$ = fromEvent<PointerEvent>(document, 'click');

click$
  .pipe(
    map(({ x, y }) => ({ x, y })),
    tap((value) => console.log('tap', value)),
    auditTime(2000)
  )
  .subscribe(console.log);
Imagen guia del contenido de la página

La diferencia práctica frente a debounceTime es que auditTime no espera silencio global para emitir. Si hay una ráfaga continua, seguirá emitiendo el último valor de cada ventana auditada. Frente a throttleTime, auditTime se enfoca en el valor final de la ventana, no en el inicial.

Info

auditTime es una buena opción para flujos de alta frecuencia donde necesitas snapshots finales por ventana: movimientos de puntero, scroll, reflujo visual o mediciones periódicas derivadas de eventos continuos.

Comparación rápida de operadores de tiempo

OperadorCuándo emiteCaso típico
debounceTimeCuando pasa un intervalo sin nuevas emisionesBúsqueda después de que el usuario deja de escribir
throttleTimeAl inicio de una ventana; opcionalmente también al finalClics, scroll o resize con respuesta inmediata
sampleTimeEn ticks periódicos, si hay un valor nuevoTelemetría o snapshots a cadencia fija
sampleCuando emite otro observable notificadorCapturar estado bajo demanda
auditTimeAl final de una ventana iniciada por la fuenteEmitir el último valor de una ráfaga temporal

Conclusión

Los operadores de tiempo RxJS permiten diseñar pipelines reactivos con políticas temporales explícitas. debounceTime reduce ruido esperando estabilidad, throttleTime limita frecuencia con respuesta inicial, sampleTime toma snapshots periódicos, sample delega el momento de lectura a otro observable y auditTime conserva el último valor de una ventana.

Elegir correctamente entre estos operadores evita temporizadores manuales, reduce estado mutable y mantiene el control de emisiones cerca del flujo que lo necesita. En el siguiente artículo de la serie abordaremos operadores de transformación para modificar la forma de los valores que viajan por el stream.

Etiquetas :