
RxJs: Operadores de tiempo
- Dacadev
- Programación , Typescript
- 3 de junio de 2026
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.

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.

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);

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.

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.

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.

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);

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
| Operador | Cuándo emite | Caso típico |
|---|---|---|
debounceTime | Cuando pasa un intervalo sin nuevas emisiones | Búsqueda después de que el usuario deja de escribir |
throttleTime | Al inicio de una ventana; opcionalmente también al final | Clics, scroll o resize con respuesta inmediata |
sampleTime | En ticks periódicos, si hay un valor nuevo | Telemetría o snapshots a cadencia fija |
sample | Cuando emite otro observable notificador | Capturar estado bajo demanda |
auditTime | Al final de una ventana iniciada por la fuente | Emitir 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.


