JavaScript: Qué es el debounce con ejemplo práctico.

Cuando trabajamos con JavaScript en el navegador, especialmente en interfaces dinámicas, es muy habitual encontrarnos con eventos que se disparan muchas veces en un periodo de tiempo muy corto.

Un usuario puede escribir varias letras consecutivas en un buscador, mover el ratón constantemente, desplazar una página con el scroll o cambiar el tamaño de una ventana. En todos estos casos, nuestro código puede recibir una enorme cantidad de eventos en muy poco tiempo.

Si cada evento provoca inmediatamente una operación costosa —como realizar una petición HTTP, ejecutar cálculos, modificar el DOM o procesar información— podemos terminar consumiendo recursos innecesariamente y degradando el rendimiento de la aplicación.

Una de las técnicas más utilizadas para solucionar este problema es debounce.

En este artículo vamos a ver qué es debounce en JavaScript, cómo funciona internamente, cómo implementarlo desde cero, cómo utilizarlo con eventos del navegador y cuáles son sus diferencias respecto a throttle.

¿Qué es debounce?

Debounce es una técnica que permite retrasar la ejecución de una función hasta que haya transcurrido un determinado periodo de tiempo desde la última llamada.

Dicho de una forma sencilla:

Mientras sigan llegando llamadas, el temporizador se reinicia. La función solamente se ejecuta cuando dejamos de recibir llamadas durante el tiempo establecido.

Por ejemplo, podemos configurar un debounce de 500 milisegundos.

Si una función recibe llamadas continuamente:

llamada
  ↓
500 ms
  ↓
llamada
  ↓
500 ms
  ↓
llamada
  ↓
500 ms
  ↓
llamada

La función real no llegará a ejecutarse porque nunca han transcurrido 500 ms sin recibir una nueva llamada.

En cambio, cuando finalmente dejamos de llamar a la función:

llamada
  ↓
llamada
  ↓
llamada
  ↓
llamada
  ↓
500 ms sin llamadas
  ↓
EJECUTAR FUNCIÓN

Esta característica hace que debounce sea especialmente útil para controlar eventos que se producen con mucha frecuencia.

¿Por qué necesitamos debounce?

Imaginemos un buscador que realiza una petición a una API cada vez que el usuario escribe una letra.

El usuario escribe:

j
ja
jav
java
javas
javasc
javascr
javascri
javascrip
javascript

Sin ningún mecanismo de control, podríamos terminar realizando una petición HTTP por cada pulsación:

GET /search?q=j
GET /search?q=ja
GET /search?q=jav
GET /search?q=java
GET /search?q=javas
GET /search?q=javasc
GET /search?q=javascr
GET /search?q=javascri
GET /search?q=javascrip
GET /search?q=javascript

En este caso probablemente no necesitamos diez peticiones.

Lo que realmente nos interesa es esperar a que el usuario termine de escribir y realizar solamente una búsqueda:

Usuario escribe
       ↓
j → ja → jav → java → javas → javascript
                                      ↓
                             deja de escribir
                                      ↓
                                   500 ms
                                      ↓
                              petición a la API

Ahí es donde entra debounce.

Implementando debounce desde cero

Una implementación básica de debounce en JavaScript puede ser sorprendentemente sencilla:

function debounce(func, delay) {
    let timeout;

    return function (...args) {
        clearTimeout(timeout);

        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

Aunque el código es pequeño, contiene varios conceptos importantes de JavaScript: closures, temporizadores, funciones de orden superior, propagación de argumentos y el contexto de ejecución mediante this.

Vamos a analizarlo.

Cómo funciona debounce paso a paso

1. Recibimos la función que queremos controlar

function debounce(func, delay) {

La función debounce() recibe dos parámetros:

  • func: función que queremos ejecutar posteriormente.
  • delay: tiempo de espera en milisegundos.

Por ejemplo:

const buscar = debounce(realizarBusqueda, 500);

Esto significa que realizarBusqueda() no se ejecutará inmediatamente cada vez que llamemos a buscar().

2. Creamos una variable para almacenar el temporizador

let timeout;

Esta variable almacenará el identificador devuelto por setTimeout().

Es importante que esta variable permanezca disponible entre las diferentes llamadas a la función. Para conseguirlo utilizamos un closure.

3. Devolvemos una nueva función

return function (...args) {

debounce() no ejecuta directamente func. En su lugar, devuelve una nueva función que será la encargada de controlar cuándo debe ejecutarse.

Por ejemplo:

const buscarConDebounce = debounce(buscarUsuarios, 500);

buscarConDebounce("javascript");

Cada llamada a buscarConDebounce() pasa por nuestro mecanismo de control.

4. Cancelamos el temporizador anterior

clearTimeout(timeout);

Esta es una de las partes fundamentales de debounce.

Si existía un temporizador pendiente, lo cancelamos.

Supongamos que el usuario escribe una letra:

Usuario escribe "j"
        ↓
setTimeout(...)
        ↓
esperando 500 ms

Pero 200 ms después escribe otra letra:

Usuario escribe "a"
        ↓
clearTimeout(...)
        ↓
se cancela el temporizador anterior
        ↓
se crea uno nuevo

De esta manera, el contador vuelve a empezar.

5. Creamos un nuevo temporizador

timeout = setTimeout(() => {
    func.apply(this, args);
}, delay);

Ahora creamos un nuevo temporizador.

Si durante el periodo establecido no vuelve a producirse otra llamada, setTimeout() ejecutará la función.

Si aparece una nueva llamada antes de que termine el tiempo, clearTimeout() cancelará este temporizador y se creará otro.

El papel de los closures

Una de las razones por las que este patrón funciona es el sistema de closures de JavaScript.

La variable:

let timeout;

pertenece al contexto creado por la función debounce().

La función que devolvemos mantiene una referencia a esa variable incluso después de que debounce() haya terminado de ejecutarse.

Por eso podemos hacer:

const buscar = debounce(realizarBusqueda, 500);

buscar("j");
buscar("ja");
buscar("jav");
buscar("java");

Las cuatro llamadas comparten el mismo timeout.

Cada nueva llamada cancela el temporizador anterior y crea uno nuevo.

¿Qué significa ...args?

En nuestra implementación encontramos:

return function (...args) {

Los tres puntos corresponden al rest parameter de JavaScript.

Permite recoger todos los argumentos enviados a la función en un array.

Por ejemplo:

function ejemplo(...args) {
    console.log(args);
}

ejemplo("JavaScript", 2026, true);

Obtendríamos:

["JavaScript", 2026, true]

Esto permite que nuestro debounce sea genérico y pueda utilizarse con funciones que reciben cualquier cantidad de argumentos.

¿Por qué utilizamos func.apply(this, args)?

Esta parte también es importante:

func.apply(this, args);

apply() permite ejecutar una función utilizando un determinado contexto this y un array de argumentos.

De esta forma, nuestro debounce puede conservar tanto los argumentos originales como el contexto desde el que se llamó.

Una implementación sencilla podría utilizar:

func(...args);

pero al utilizar:

func.apply(this, args);

conservamos explícitamente el contexto de ejecución.

Ejemplo básico con un evento input

Ahora podemos utilizar nuestro debounce con un campo de texto.

<input
    type="text"
    id="search"
    placeholder="Escribe algo..."
>

En JavaScript:

function buscar(texto) {
    console.log("Buscando:", texto);
}

function debounce(func, delay) {
    let timeout;

    return function (...args) {
        clearTimeout(timeout);

        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

const input = document.getElementById("search");

const buscarConDebounce = debounce(function (event) {
    buscar(event.target.value);
}, 500);

input.addEventListener("input", buscarConDebounce);

Ahora la función de búsqueda solamente se ejecutará cuando el usuario haya dejado de escribir durante 500 milisegundos.

Ejemplo completo: buscador conectado a una API

Vamos a utilizar ahora un ejemplo más realista. Tenemos un buscador que obtiene usuarios desde una API y muestra los resultados.

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Buscador con Debounce</title>

    <style>
        body {
            font-family: Arial, sans-serif;
            padding: 40px;
        }

        input {
            width: 300px;
            padding: 10px;
            font-size: 16px;
        }

        #results {
            margin-top: 20px;
        }

        #status {
            margin-top: 10px;
            color: #666;
        }
    </style>
</head>

<body>

    <h1>Buscador de usuarios</h1>

    <input
        type="text"
        id="search"
        placeholder="Escribe un nombre..."
        autocomplete="off"
    >

    <div id="status"></div>

    <ul id="results"></ul>

    <script>

        function debounce(func, delay) {
            let timeout;

            return function (...args) {
                clearTimeout(timeout);

                timeout = setTimeout(() => {
                    func.apply(this, args);
                }, delay);
            };
        }

        async function buscarUsuarios(query) {

            const results = document.getElementById("results");
            const status = document.getElementById("status");

            if (!query.trim()) {
                results.innerHTML = "";
                status.textContent = "";
                return;
            }

            status.textContent = "Buscando...";

            try {

                const response = await fetch(
                    "https://jsonplaceholder.typicode.com/users"
                );

                if (!response.ok) {
                    throw new Error("Error al obtener los datos");
                }

                const users = await response.json();

                const filtrados = users.filter(user =>
                    user.name
                        .toLowerCase()
                        .includes(query.toLowerCase())
                );

                if (filtrados.length === 0) {

                    results.innerHTML =
                        "<li>No se encontraron resultados</li>";

                } else {

                    results.innerHTML = filtrados
                        .map(user => `<li>${user.name}</li>`)
                        .join("");

                }

                status.textContent =
                    `${filtrados.length} resultado(s)`;

            } catch (error) {

                console.error(error);

                status.textContent =
                    "Se produjo un error al realizar la búsqueda.";
            }
        }

        const input = document.getElementById("search");

        input.addEventListener(
            "input",
            debounce(function (event) {

                buscarUsuarios(event.target.value);

            }, 500)
        );

    </script>

</body>

</html>

¿Qué está ocurriendo en este ejemplo?

El navegador genera un evento input cada vez que cambia el contenido del campo de texto.

En lugar de ejecutar directamente:

buscarUsuarios(event.target.value);

utilizamos:

debounce(function (event) {
    buscarUsuarios(event.target.value);
}, 500);

Esto introduce una espera de 500 ms.

Si el usuario escribe:

j
ja
jav
java
javasc
javascript

cada pulsación reinicia el temporizador.

Solamente cuando el usuario deja de escribir durante medio segundo se ejecuta la búsqueda.

Sin debounce vs. con debounce

Sin debounce

input.addEventListener("input", function (event) {
    buscarUsuarios(event.target.value);
});

Si el usuario escribe diez caracteres, podemos llegar a ejecutar la búsqueda diez veces.

Con debounce

input.addEventListener(
    "input",
    debounce(function (event) {
        buscarUsuarios(event.target.value);
    }, 500)
);

Ahora la función solamente se ejecutará después de que el usuario deje de escribir durante 500 ms.

En determinadas aplicaciones esto puede reducir considerablemente el número de operaciones realizadas.

Debounce y peticiones HTTP

Uno de los casos de uso más habituales es controlar peticiones HTTP.

Sin debounce:

Usuario escribe
        ↓
Evento input
        ↓
fetch()
        ↓
Usuario escribe
        ↓
Evento input
        ↓
fetch()
        ↓
Usuario escribe
        ↓
Evento input
        ↓
fetch()

Con debounce:

Usuario escribe
        ↓
Evento input
        ↓
reiniciar temporizador
        ↓
Usuario escribe
        ↓
Evento input
        ↓
reiniciar temporizador
        ↓
Usuario deja de escribir
        ↓
esperar 500 ms
        ↓
fetch()

Esto reduce el número de peticiones y puede mejorar tanto el rendimiento del navegador como la carga del servidor.

Debounce no cancela una petición HTTP que ya comenzó

Hay una distinción importante que conviene conocer.

Debounce controla cuándo se inicia una función. No cancela automáticamente una operación que ya se haya iniciado.

Por ejemplo, si hacemos:

fetch("/api/search?q=javascript");

y la petición ya ha sido enviada al servidor, llamar posteriormente a clearTimeout() no puede deshacer esa petición.

Si necesitamos cancelar peticiones HTTP anteriores podemos combinar debounce con AbortController.

Debounce + AbortController

let controller = null;

async function buscar(query) {

    if (controller) {
        controller.abort();
    }

    controller = new AbortController();

    try {

        const response = await fetch(
            `/api/search?q=${encodeURIComponent(query)}`,
            {
                signal: controller.signal
            }
        );

        const data = await response.json();

        console.log(data);

    } catch (error) {

        if (error.name === "AbortError") {
            console.log("Petición cancelada");
            return;
        }

        console.error(error);
    }
}

De esta manera podemos controlar dos problemas diferentes:

  • Debounce: evita iniciar peticiones innecesarias mientras el usuario continúa escribiendo.
  • AbortController: permite cancelar una petición que ya está en curso cuando deja de ser necesaria.

¿Cuánto tiempo debemos utilizar como delay?

No existe un valor universal. Depende del tipo de interacción.

Situación Delay aproximado
Búsqueda instantánea 200–300 ms
Autocompletado 250–400 ms
Búsquedas contra API 300–500 ms
Validación de formularios 300–600 ms
Operaciones costosas 500 ms o más

Estos valores son solamente una referencia. El tiempo adecuado depende de la aplicación y de la experiencia de usuario que queramos conseguir.

¿Dónde podemos utilizar debounce?

Debounce resulta especialmente útil cuando una acción puede producir muchos eventos consecutivos y solamente nos interesa reaccionar cuando la actividad se detiene.

Buscadores

Es probablemente uno de los casos más conocidos.

input.addEventListener(
    "input",
    debounce(actualizarResultados, 300)
);

Validación de formularios

Podemos utilizarlo para evitar ejecutar una validación compleja con cada pulsación.

input.addEventListener(
    "input",
    debounce(validarCampo, 400)
);

Redimensionamiento de ventana

El evento resize puede producir una gran cantidad de eventos mientras el usuario cambia el tamaño de la ventana.

window.addEventListener(
    "resize",
    debounce(recalcularLayout, 250)
);

Autoguardado

También podemos utilizar debounce para implementar sistemas de autoguardado.

const guardarCambios = debounce(() => {
    console.log("Guardando cambios...");
}, 1000);

Así evitamos enviar una operación de guardado cada vez que el usuario modifica algo.

Debounce también puede utilizarse fuera del navegador

Aunque el ejemplo más habitual está relacionado con eventos del navegador, el concepto de debounce no es exclusivo del DOM.

La idea general puede aplicarse siempre que tengamos una operación que se dispara repetidamente y queramos esperar hasta que la actividad se estabilice antes de ejecutarla.

Es un patrón que podemos encontrar en interfaces gráficas, aplicaciones de escritorio, servidores, sistemas reactivos y diferentes arquitecturas basadas en eventos.

Debounce vs. Throttle

Debounce suele confundirse con otra técnica llamada throttle.

Ambas técnicas sirven para controlar la frecuencia con la que se ejecuta una función, pero funcionan de forma diferente.

Técnica Comportamiento Uso habitual
Debounce Espera hasta que deje de haber llamadas durante un periodo determinado. Buscadores, autoguardado, validación.
Throttle Limita la ejecución a una vez cada determinado intervalo. Scroll, mousemove, seguimiento de posición.

Ejemplo conceptual

Debounce:

evento evento evento evento evento
                                      ↓
                                  espera
                                      ↓
                                  EJECUTA

Throttle:

evento → EJECUTA
evento
evento
evento
evento → EJECUTA
evento
evento
evento → EJECUTA

Por tanto:

Debounce espera a que termine la actividad.

Throttle limita la frecuencia durante la actividad.

¿Cuándo utilizar debounce y cuándo throttle?

Una regla práctica puede ser la siguiente:

  • Si quieres ejecutar algo cuando el usuario termine de hacer algo, probablemente necesitas debounce.
  • Si quieres ejecutar algo periódicamente mientras el usuario continúa realizando una acción, probablemente necesitas throttle.

Por ejemplo, para un buscador:

Escribir → debounce

Para actualizar una interfaz mientras hacemos scroll:

Scroll → throttle

Una implementación de debounce más completa

La implementación básica es suficiente para muchos casos, pero podemos añadir algunas funcionalidades útiles, como permitir cancelar el temporizador pendiente.

function debounce(func, delay) {

    let timeout;

    function debounced(...args) {

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);

    }

    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = undefined;
    };

    return debounced;
}

Ahora podemos hacer:

const buscar = debounce(realizarBusqueda, 500);

buscar("javascript");

// Cancelar ejecución pendiente
buscar.cancel();

Esto puede resultar útil cuando destruimos un componente, abandonamos una pantalla o ya no necesitamos ejecutar la operación pendiente.

Errores habituales al utilizar debounce

Crear un debounce nuevo en cada evento

Uno de los errores más comunes consiste en hacer algo parecido a esto:

input.addEventListener("input", function (event) {

    debounce(buscar, 500)(event.target.value);

});

Esto crea una nueva instancia de debounce en cada evento. Como consecuencia, cada instancia tiene su propio temporizador y no estamos consiguiendo el comportamiento esperado.

Es mejor crear la función debounced una sola vez:

const buscarConDebounce = debounce(buscar, 500);

input.addEventListener("input", buscarConDebounce);

Utilizar un delay excesivamente grande

Un debounce de varios segundos puede reducir mucho las operaciones, pero también puede hacer que la interfaz parezca lenta.

El usuario debería percibir la aplicación como rápida y responsiva.

Confundir debounce con throttle

Ambas técnicas solucionan problemas relacionados con eventos frecuentes, pero no son intercambiables en todos los escenarios.

Para una búsqueda normalmente queremos esperar a que el usuario deje de escribir.

Para un evento de scroll, en cambio, puede interesarnos continuar procesando información mientras el usuario se desplaza, pero limitando la frecuencia.

Debounce y rendimiento en aplicaciones web

Debounce no hace que una función sea intrínsecamente más rápida.

Lo que hace es reducir el número de veces que esa función necesita ejecutarse.

Esto es especialmente importante cuando la función realiza operaciones costosas, como:

  • Peticiones HTTP.
  • Consultas a APIs.
  • Cálculos complejos.
  • Manipulación intensiva del DOM.
  • Filtrado de grandes cantidades de datos.
  • Procesamiento de imágenes.
  • Actualización de componentes de una interfaz.

Por tanto, debounce debe entenderse como una técnica de control de frecuencia de ejecución, no como una optimización mágica del código.

Resumen visual

Sin debounce:

EVENTO → FUNCIÓN
EVENTO → FUNCIÓN
EVENTO → FUNCIÓN
EVENTO → FUNCIÓN
EVENTO → FUNCIÓN
EVENTO → FUNCIÓN

Con debounce:

EVENTO
   ↓
reiniciar temporizador

EVENTO
   ↓
reiniciar temporizador

EVENTO
   ↓
reiniciar temporizador

EVENTO
   ↓
500 ms sin eventos
   ↓
FUNCIÓN

Conclusión

Debounce es una técnica sencilla pero extremadamente útil para controlar funciones asociadas a eventos que se producen con mucha frecuencia.

Su funcionamiento se basa en una idea muy simple: cada nueva llamada cancela el temporizador anterior y comienza uno nuevo. La función solamente se ejecuta cuando transcurre el periodo configurado sin recibir nuevas llamadas.

Esto lo convierte en una herramienta especialmente útil para buscadores, autocompletado, validación de formularios, autoguardado, eventos de redimensionamiento y cualquier otra interacción en la que queramos esperar a que el usuario termine una acción antes de procesarla.

También es importante recordar que debounce y throttle no son lo mismo. Debounce espera a que la actividad se detenga, mientras que throttle limita la frecuencia de ejecución mientras la actividad continúa.

Una vez comprendido este patrón, podemos utilizarlo para construir interfaces JavaScript más eficientes, reducir operaciones innecesarias y mejorar la experiencia de usuario sin necesidad de recurrir a soluciones complejas.


En pocas palabras: si una función se está ejecutando demasiadas veces porque un evento se dispara continuamente, probablemente necesites controlar su frecuencia. Y si lo que quieres es ejecutar la función solamente cuando la actividad haya terminado, debounce es una de las soluciones más sencillas y efectivas que ofrece JavaScript.

Visitas: 15

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar