El objeto de configuración en addEventListener
Cuando utilizamos addEventListener, existe un tercer parámetro opcional que acepta un objeto con varias propiedades de configuración. Estas propiedades nos permiten controlar el comportamiento del listener de forma precisa.
Propiedades disponibles
- capture: Valor booleano que determina si el listener se ejecuta en la fase de captura o en la fase de propagación (bubbling). Por defecto es
false, lo que significa que se ejecuta en la fase de propagación. - once: Cuando se establece en
true, el listener se ejecutará una sola vez y luego se eliminará automáticamente. - passive: Al establecerlo en
true, se le indica al navegador que el listener nunca invocarápreventDefault(). Esto tiene implicaciones importantes en el rendimiento. - signal: Permite asociar un objeto
AbortSignalpara poder cancelar el listener cuando se llame al métodoabort()del controlador correspondiente.
Ejemplo práctico
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demostración de opciones en addEventListener</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
.container {
width: 200px;
height: 200px;
background-color: lightblue;
margin: 20px;
}
</style>
</head>
<body>
<div class="container"></div>
<button id="cancelBtn">Cancelar</button>
<script>
const container = document.querySelector('.container');
const cancelBtn = document.querySelector('#cancelBtn');
// Configuración con capture
container.addEventListener('click', (ev) => {
console.log('Fase de captura: clic en container');
}, { capture: true });
// Configuración con once
container.addEventListener('click', (ev) => {
console.log('Una sola vez: clic en container');
}, { once: true });
// Configuración con passive
container.addEventListener('touchstart', (ev) => {
console.log('Passive: inicio de toque en container');
// ev.preventDefault(); // No tendrá efecto
}, { passive: true });
// Configuración con signal
const abortCtrl = new AbortController();
const { signal } = abortCtrl;
container.addEventListener('click', (ev) => {
console.log('Signal: clic en container');
}, { signal });
cancelBtn.addEventListener('click', () => {
abortCtrl.abort();
console.log('Cancelado: listener removido');
});
</script>
</body>
</html>
Optimización de rendimiento con passive
La propiedad passive juega un papel crucial en la optimización del rendimiento de desplazamiento en páginas web.
El rol de preventDefault
En JavaScript, ciertos eventos desencadenan comportamientos predeterminados del navegador, como la navegación al hacer clic en un enlace o la entrada de texto al presionar teclas. El método preventDefault() permite cancelar estos comportamientos predeterminados.
Regiones de desplazamiento no rápido
El concepto de regiones de desplazamiento no rápido (non-fast scrollable regions) es fundamental para entender cómo el navegador gestiona la composición de la página. Cuando se registra un listener de eventos, el hilo de composición marca esa área como una región de desplazamiento no rápido.
Cuando un evento ocurre en estas regiones, el hilo de composición debe:
- Enviar el evento al hilo principal para su procesamiento
- Esperar a que el hilo principal complete la ejecución antes de continuar con la composición de nuevos fotogramas
Esto puede causar varios problemas:
- Si la función de manejo de eventos tarda demasiado, los nuevos fotogramas se retrasan, provocando una sensación de tartamudeo
- Si se llama a
preventDefault(), el hilo de composición detiene la creación de nuevos fotogramas, eliminando el efecto de desplazamiento - Si el evento ocurre fuera de estas regiones, el hilo de composición puede generar nuevos fotogramas sin esperar al hilo principal
Al usar passive: true, se le indica al navegador que el listener no cancelará el comportamiento predeterminado, permitiendo que el hilo de composición continúe generando fotogramas sin esperar la respuesta del hilo principal.
Simulación de problemas de rendimiento
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demostración de tartamudeo</title>
<style>
.content {
height: 3000px;
background: linear-gradient(70deg, blue, pink);
}
</style>
</head>
<body>
<div class="content"></div>
<script>
function blockThread(duration) {
const start = Date.now();
while (Date.now() < start + duration) {}
}
document.addEventListener(
"wheel",
(ev) => {
if (Math.random() < 0.8) {
console.log("Iniciando bloqueo ---------");
blockThread(500);
console.log("Fin del bloqueo ---------");
console.log("\n");
}
},
{ passive: false }
);
</script>
</body>
</html>
En este código, se escucha el evento wheel y hay un 80% de probabilidad de ejecutar una tarea de 500ms, lo que provoca tartamudeo durante el desplazamiento. Al cambiar passive a true, el desplazamiento se vuelve fluido porque el hilo de composición no necesita esperar al hilo principal.
Diferencias entre eventos scroll y wheel
Frecuencia de activación
- scroll: Se dispara con menor frecuencia, típicamente cuando el desplazamiento se detiene.
- wheel: Se dispara con alta frecuencia, especialmente durante desplazamientos rápidos.
Mecanismo de procesamiento
- scroll: Al dispararse después del desplazamiento, las operaciones costosas tienen menor impacto en la experiencia.
- wheel: Debido a su alta frecuencia, las operaciones costosas bloquean el hilo principal y provocan tartamudeo.
Optimizaciones del navegador
Los navegadores modernos han optimizado el evento scroll para ofrecer una experiencia fluida incluso sin la opción passive. Sin embargo, el evento wheel no cuenta con estas optimizaciones, por lo que es más propenso a causar problemas de rendimiento.
Escenarios de uso recomendados
Eventos scroll
Botón de volver arriba: Mostrar un botón cuando el usuario se desplaza más allá de cierta posición.
const btnTop = document.getElementById('btnTop');
window.addEventListener('scroll', () => {
btnTop.style.display = window.scrollY > 300 ? 'block' : 'none';
});
btnTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
Carga infinita: Cargar contenido adicional cuando el usuario llega al final de la página.
window.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight) {
fetchMoreContent();
}
});
Eventos wheel
- Zoom de elementos: Permitir acercar o alejar imágenes mediante la rueda del mouse
- Detección de dirección: Determinar si el usuario se desplaza hacia arriba o hacia abajo