Lección 04

¿Por qué escribir en un buscador congela la página?

Cada tecla dispara trabajo en el único carril que también atiende clics. La salida no es escribir más rápido, sino hacer menos trabajo por tecla.

01Qué es una tarea larga

El navegador hace el trabajo de tu página en el hilo principal: un solo carril donde corre tu JavaScript, se recalculan los estilos, se calcula el layout y se pinta. Un carril, no cuatro.

Una tarea es un tramo de trabajo que el navegador empieza y termina sin soltar el carril. Una tarea larga es la que pasa de 50 ms, el umbral con el que se dice "esto ya se nota". Por debajo, el usuario no lo distingue de la normalidad. Por encima, el clic que llega mientras corre se queda en cola: no se pierde, espera. Si espera 400 ms, la página parece muerta.

Una tarea de 400 ms se queda con el único carril hilo principal buscar en 43.200 productos pintar pintar 400 ms sin atender nada más el clic llega aquí y espera a que el carril quede libre
El clic no se procesa al llegar: se encola detrás de la tarea. La sensación de congelamiento es ese tiempo de espera.

02Por qué sucede: hacer menos veces no es hacer menos

Un buscador de 43.200 productos recorre la lista y compara: normaliza cada nombre (minúsculas, acentos fuera) y comprueba si contiene lo escrito. Ese recorrido cuesta lo mismo con una letra que con veinte, porque siempre mira todos.

Escribes "cami": cuatro teclas, cuatro recorridos completos. Si cada uno tarda 120 ms, la cuarta llega cuando el navegador va por la tercera, y la cola crece. A 400 ms por búsqueda, la página parece congelada mientras terminas la palabra.

Aquí se confunden dos cosas: hacer menos veces y hacer menos trabajo. El debounce es lo primero: espera 250 ms de silencio y ejecuta una sola vez. Pasas de seis ejecuciones a una o dos. Pero esa ejecución sigue tardando 400 ms, así que el congelamiento no desaparece: se muda a después de la última tecla. El debounce no arregla el algoritmo; solo cambia cuándo lo llamas.

El atajo que no arregla nada

Bajar el debounce a 50 ms "para que responda antes" devuelve el problema: vuelves a ejecutar en casi cada tecla.

Menos veces frente a menos trabajo sin nada 6 × 400 ms debounce 2 × 400 ms índice 6 × ~1 ms worker hilo trabajador caro de copiar
Las dos primeras filas reducen cuántas veces se ejecuta; las dos últimas, cuánto ocupa cada ejecución. Solo las de abajo quitan el área roja del carril principal.
El worker calcula fuera; el DOM se queda en el hilo principal hilo principal · dueño del DOM worker · otro pasillo DOM pintar tecla → handler índice buscar devuelve ids el DOM se queda aquí no ve el documento índice, una vez ids, decenas copiar cuesta; el DOM no cruza
El worker no toca la página: recibe datos copiados y devuelve ids. Por eso conviene mandar el índice una vez y no la lista en cada tecla.

03Cómo se ataca: menos trabajo por tecla

La primera palanca es un índice precalculado: en vez de recorrer 43.200 productos por tecla, construyes una vez un mapa de palabra a ids. Pagas 150-200 ms al cargar y cada búsqueda es una consulta a una tabla: del orden de un milisegundo.

La segunda es la estructura de datos. Si las búsquedas son por prefijo, un índice de prefijos o un árbol de letras evita mirar los que no empiezan así; si la lista está ordenada, una búsqueda binaria encuentra el rango en pocos saltos. Cambiar la forma de guardar suele rendir más que optimizar el bucle.

La tercera es un Web Worker, que se entiende como otro pasillo. El navegador abre un hilo aparte y ejecuta ahí un archivo tuyo, en paralelo al principal. Lo que no puede hacer es tocar el DOM: el documento pertenece al hilo principal y el worker no lo ve. Se hablan por mensajes: postMessage envía y onmessage recibe, y los datos se copian en el camino (clonado estructurado), no se comparten.

Esa copia es la factura: mandar 43.200 objetos por tecla puede costar más que buscarlos, así que se manda el índice una vez y el worker devuelve solo ids. Por eso un worker no siempre vale la pena: si la tarea dura 20 ms o el cuello es pintar 1.000 resultados, el trabajo sigue en el carril principal. Si es enorme pero divisible, a veces basta cortarla en trozos de 8 ms.

// Una vez, al cargar: normaliza y arma el índice
const indice = new Map();      // palabra -> ids de productos

for (const p of productos) {   // ~43.200 productos, una vez
  for (const palabra of normalizar(p.nombre).split(' ')) {
    if (!indice.has(palabra)) indice.set(palabra, []);
    indice.get(palabra).push(p.id);
  }
}

function normalizar(texto) {   // "Camión" y "camion" son iguales
  return texto.toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');
}

// En cada tecla: una consulta, no 43.200 comparaciones
function buscar(consulta) {
  const clave = normalizar(consulta.trim());
  if (!clave) return [];
  // decenas de ids, no la lista entera
  return indice.get(clave) ?? [];
}

// Un prefijo como "cam" no está en el índice: se recorren
// las claves (muchas menos que productos) o se guardan.

04Cómo diagnosticarlo

Antes de elegir arreglo, mide el tamaño de la tarea: no es lo mismo 40 ms que 400.

HerramientaQué se miraCómo se ve el síntoma
Performance › pista Main La barra de la tarea que dispara la tecla y su triángulo rojo. Un bloque de 200-600 ms por tecla; los clics aparecen como espera antes de su handler.
Performance › CPU 4x más lenta La misma grabación con el procesador frenado. La tarea pasa de 100 a 400 ms y el problema se reproduce en tu máquina rápida.
performance.now() Los ms entre el inicio y el fin del handler, cinco veces. Números estables de cientos de ms, sin mirar el panel.
Performance › Bottom-Up Qué función consume el tiempo dentro de la tarea. El 80 % en la normalización y el recorrido, no en pintar: ahí está el algoritmo.
Regla práctica

Mide en frío y con caché caliente. El primer trazo siempre sale peor; el número que importa es el de la segunda y tercera tecla, cuando el usuario ya va escribiendo.

Para decirlo en la entrevista

“The main thread is a single lane: any task over 50 ms blocks input and paint.”

“Debouncing reduces how often the work runs; it does not reduce how long one run takes.”

“I precompute an index so a keystroke is a lookup, and move work to a worker only when the copy is cheaper than the compute.”

Comprueba que lo tienes

Cuatro preguntas. Al comprobar se marca la lección como completada.

Escribes "cami" y la página se queda muda ~400 ms. ¿Qué pasa?

Pones un debounce de 300 ms. ¿Qué mejora y qué no?

Mueves la búsqueda a un Web Worker. ¿Cuál es el costo real de esa decisión?

El índice tarda ~180 ms en construirse al cargar. ¿Cómo se justifica ese costo?