Enunciado y escenarios aplicables
Una aplicación de analítica de mismo origen tiene tres requisitos independientes:
- Procesar localmente un archivo CSV de 200 MiB seleccionado por el usuario mientras la entrada y el renderizado se mantienen responsivos.
- Compartir un único WebSocket y el estado de suscripción en memoria entre pestañas mientras al menos una pestaña permanezca abierta.
- Abrir el app shell y el último reporte exitoso estando offline, encolar un guardado y reintentarlo una vez restablecida la conectividad.
Elige entre Dedicated Worker, SharedWorker y Service Worker para cada requisito. Explica la pertenencia (ownership), tiempo de vida, comunicación, persistencia, manejo de cancelaciones y fallos, fallbacks para navegadores, límites de seguridad y validación.
El archivo de 200 MiB, el WebSocket único y el guardado offline son supuestos para la entrevista, no objetivos universales de producto. Esta pregunta es adecuada para roles de frontend senior, plataforma Web y arquitectura frontend. Su habilidad principal es la ejecución en el navegador y la selección de la plataforma Web, por lo que la categoría es frontend.
Qué evalúa el entrevistador
Primero, ¿puede el candidato elegir en función de quién es el propietario del trabajo, cuánto tiempo debe vivir y a qué páginas afecta, en lugar de llamar a cualquier hilo en segundo plano simplemente Web Worker? Una respuesta sólida asigna el cómputo propio de la página a un Dedicated Worker, el estado temporal de múltiples páginas de mismo origen a un SharedWorker, y la intermediación de red con ámbito (scoped network proxying), almacenamiento en caché y trabajo en segundo plano orientado a eventos a un Service Worker.
Segundo, ¿puede el candidato separar el multihilo de la persistencia? Mover código fuera del hilo principal no reduce automáticamente el uso de CPU, memoria o I/O. La memoria de un SharedWorker no es un estado duradero. Un navegador puede detener un Service Worker entre eventos, por lo que el trabajo pendiente debe residir en un almacenamiento duradero como IndexedDB.
Tercero, ¿puede el candidato cuantificar el costo de la mensajería? La clonación estructurada normalmente copia los datos. Un ArrayBuffer puede transferirse para que su propiedad se mueva sin otra copia byte por byte, pero el búfer del emisor queda desasociado (detached). El manejo de CSV grandes también necesita división en fragmentos (chunking), progreso, contrapresión (backpressure) y cancelación.
Cuarto, ¿puede el candidato proporcionar un fallback correcto? SharedWorker alcanzó Baseline 2026 en las versiones actuales de los navegadores, pero dispositivos antiguos y algunas implementaciones podrían no soportarlo. Background Sync aún no es Baseline. La correctitud no puede depender de una única API opcional.
Quinto, ¿puede el candidato probar fallos en el ciclo de vida? Un simple recorrido por el camino feliz no es suficiente. Las recargas, cerrar la última pestaña, las actualizaciones del Service Worker, los reinicios offline, los eventos de sincronización duplicados, la contaminación de caché y los fallbacks para navegadores antiguos son todos aspectos importantes.
Preguntas para aclarar primero
- ¿El procesamiento de CSV es ocasional o continuo? Un trabajo puntual puede crear un Dedicated Worker bajo demanda. El trabajo concurrente y continuo necesita un grupo limitado de workers (worker pool) y una cola, en lugar de un hilo nuevo por cada archivo.
- ¿Debe estar toda la entrada en memoria? Prefiere la entrada por streaming o por fragmentos cuando el parser lo admita. Si todo el
ArrayBufferdebe moverse a la vez, presupuesta por separado los bytes sin procesar, las cadenas decodificadas, los valores parseados y los índices. - ¿Todas las pestañas son exactamente del mismo origen? SharedWorker requiere el mismo esquema, host y puerto. Los subdominios, los iframes de terceros o diferentes puertos de desarrollo alteran el diseño de la comunicación.
- ¿Un único WebSocket es una optimización o una restricción de correctitud? Si solo ahorra conexiones, una conexión por pestaña es un fallback válido. Si el servidor permite únicamente una sesión, el fallback necesita BroadcastChannel, elección de líder, concesiones (leases) y recuperación ante situaciones de cerebro dividido (split-brain).
- ¿Un guardado offline puede reintentarse automáticamente? Las operaciones sensibles o con posibilidad de conflicto pueden requerir confirmación del usuario después de que la página vuelva a estar activa. Los reintentos automáticos necesitan una clave de idempotencia, estado duradero y retroceso exponencial limitado (bounded backoff).
- ¿Qué navegadores antiguos y modos de privacidad están dentro del alcance? Esa respuesta define la detección de características y el límite de fallback para SharedWorker, Background Sync, cuota de almacenamiento y comportamiento de Service Worker.
Marco de respuesta de 30 segundos
“Dividiría los requisitos por pertenencia y tiempo de vida. El CSV de 200 MiB es un trabajo intensivo de CPU que pertenece a la página actual, por lo que usaría un Dedicated Worker, parsearía en fragmentos, reportaría el progreso, transferiría el ArrayBuffer cuando se pueda ceder la propiedad, y cancelaría de forma cooperativa o terminaría el worker. Colocaría el único WebSocket compartido entre pestañas de mismo origen en un SharedWorker y permitiría que cada página se suscriba mediante un MessagePort, con detección de características y un fallback a BroadcastChannel más elección de líder o, si es aceptable, una conexión por pestaña. Utilizaría un Service Worker para el shell offline, la caché de reportes y el reintento posterior, ya que puede interceptar peticiones con ámbito y manejar eventos en segundo plano. La cola se almacena en IndexedDB con claves de idempotencia. Sin Background Sync, la página la procesa al iniciar, al pasar a primer plano o al reconectarse. Probaría el cierre de la última pestaña, el reinicio offline, la sincronización duplicada y las actualizaciones del Service Worker”.
Análisis detallado paso a paso
Paso 1: Elegir según pertenencia, ámbito y tiempo de vida
| Requisito | Elección | Propietario y comunicación | Límite principal de fallo |
|---|---|---|---|
| Parsear un CSV grande para la página actual | Dedicated Worker | Página creadora; postMessage | Cierre de página, cancelación, pico de memoria |
| Compartir una conexión entre pestañas del mismo origen | SharedWorker | Páginas del mismo origen; un MessagePort por página | Cierre de la última referencia, falta de soporte en navegadores antiguos |
| Shell offline, proxy de red, sincronización posterior | Service Worker | Origen registrado y ámbito de ruta; eventos y mensajes de clientes | Actualizaciones en espera, terminación entre eventos, API opcional no disponible |
Ninguno de los tres puede manipular el DOM directamente. Dedicated Worker y SharedWorker ejecutan código JavaScript requerido por las páginas. Un Service Worker es un proxy de red orientado a eventos registrado para un origen y una ruta. Puede gestionar fetch, almacenamiento en caché, Push y sincronización en segundo plano en navegadores compatibles, pero no es adecuado para un procesamiento de CSV que deba ejecutarse de forma continua durante minutos.
Paso 2: Aislar el procesamiento del CSV de 200 MiB en un Dedicated Worker
Crea el Dedicated Worker bajo demanda, ya que la tarea sirve únicamente a la página actual. El hilo principal es dueño de la selección de archivos, la interfaz de usuario de progreso y el renderizado de resultados. El worker es dueño de la decodificación, tokenización, inferencia de tipos y agregación. Al no tener acceso al DOM, devuelve los resultados a través de mensajes.
Este esquema de interfaz omite el parser y los tipos de error. chunkBytes indica al worker que avance internamente en lotes de 4 MiB; esto no significa que el hilo principal haga otra copia:
const parser = new Worker(
new URL("./csv-parser.worker.js", import.meta.url),
{ type: "module" },
);
const jobId = crypto.randomUUID();
const buffer = await file.arrayBuffer();
parser.postMessage(
{ type: "parse", jobId, buffer, chunkBytes: 4 * 1024 * 1024 },
[buffer],
);
parser.onmessage = ({ data }) => {
if (data.jobId !== jobId) return;
if (data.type === "progress") renderProgress(data.rows);
if (data.type === "done") renderReport(data.summary);
};
cancelButton.onclick = () => {
parser.postMessage({ type: "cancel", jobId });
};La cancelación puede ser cooperativa o forzada. El bucle de análisis comprueba una bandera de cancelación en los límites de cada fragmento, libera los objetos temporales y reporta un estado final. Si el worker se bloquea o el parser no puede ceder el control, terminate() lo detiene, pero se perderán todos los trabajos dentro de ese worker. Por lo tanto, un procesador de múltiples archivos de larga duración necesita aislamiento de tareas en lugar de un único worker compartido sin diferenciación.
Paso 3: Diseñar para la copia y el pico de memoria
El postMessage ordinario utiliza clonación estructurada. Bajo la suposición simplificada de que existe un ArrayBuffer de 200 MiB tanto en el emisor como en el receptor, los bytes sin procesar ocupan por sí solos alrededor de 400 MiB. Esto excluye el almacenamiento de respaldo de File, las cadenas decodificadas, los objetos de fila, los índices y el margen para el recolector de basura. Esta es una deducción teórica para la entrevista, no una garantía de memoria del navegador.
Colocar el ArrayBuffer en la lista de transferencia mueve su recurso de memoria al worker y desasocia el búfer del emisor, evitando esa copia byte a byte. No lo transfieras si la página aún necesita el original y no se ha planificado la transferencia de propiedad. Una vía más segura para entradas grandes consiste en leer Blob.stream() o fragmentos, enviar trozos pequeños y recibir estadísticas incrementales. El hilo principal limita los fragmentos en tránsito y envía el siguiente solo después de una confirmación, generando contrapresión.
La memoria compartida no es el atajo por defecto. SharedArrayBuffer requiere aislamiento de origen cruzado (cross-origin isolation) e introduce operaciones atómicas, condiciones de carrera de datos y una seguridad de despliegue más estricta. No añadas esa complejidad antes de que las mediciones demuestren que los mensajes ordinarios y los objetos transferibles son el cuello de botella.
Paso 4: Mantener la conexión multipestaña de mismo origen en un SharedWorker
SharedWorker cumple con “mantener una única instancia compartida mientras al menos una página de mismo origen permanezca conectada”. Cada pestaña abre el mismo worker con nombre en la misma URL y registra suscripciones a través de su propio MessagePort. El worker gestiona el WebSocket, un conjunto de puertos y un mapa de suscripciones en memoria, enrutando luego los mensajes del servidor a los puertos interesados.
const socketHub = new SharedWorker(
new URL("./socket-hub.shared-worker.js", import.meta.url),
{ type: "module", name: "analytics-socket-hub" },
);
socketHub.port.start();
socketHub.port.postMessage({ type: "subscribe", reportId });
socketHub.port.onmessage = ({ data }) => applyLiveUpdate(data);
window.addEventListener("pagehide", () => {
socketHub.port.postMessage({ type: "disconnect", reportId });
socketHub.port.close();
});Únicamente los contextos con exactamente el mismo origen pueden acceder al SharedWorker. Puede mantenerse activo mientras una página abierta mantenga una referencia; una vez cerrada la última referencia, no funciona como un servicio permanente en segundo plano. La pérdida de conexión, el cierre del navegador o una caída del worker pueden borrar el mapa de suscripciones. Persiste los cursores del servidor que deban conservarse o haz que cada página vuelva a declarar sus suscripciones.
Paso 5: Proporcionar un fallback real para SharedWorker
Detecta la característica con SharedWorker. Sin ella, las pestañas pueden intercambiar señales de vida (heartbeats) y eventos a través de BroadcastChannel, mientras que Web Locks o una concesión de almacenamiento que expira elige a un líder para gestionar el WebSocket. Otras pestañas eligen un reemplazo cuando el líder se cierra. El servidor deduplica por ID de sesión y de evento, mientras que los clientes rechazan mensajes de un líder antiguo usando una época monotónica para mitigar los daños por split-brain.
Ese fallback es sustancialmente más complejo que un SharedWorker. Si una única conexión es solo una optimización de costos, el fallback correcto más simple es una conexión WebSocket por pestaña con idempotencia en el servidor y límites de conexión. Implementa la elección, la renovación de concesiones, la detección de fallos y la inyección de fallas solo cuando una restricción estricta del servidor obligue a usar una sola conexión.
Paso 6: Usar un Service Worker para el comportamiento offline y los reintentos
Un Service Worker se registra contra un origen y un ámbito de ruta, y puede interceptar solicitudes de páginas controladas. El app shell se adapta bien al precaching versionado o cache-first. El reporte más reciente a menudo se adapta a network-first: actualiza Cache Storage ante una respuesta de red exitosa y devuelve la última respuesta en caché en caso de fallo. Guarda la cola de envíos del usuario en IndexedDB, no en Cache Storage ni en una variable global del Service Worker.
Este sigue siendo un boceto de interfaz. El código de producción debe incluir en lista blanca las URLs almacenables en caché, validar las respuestas, versionar las cachés y escribir el guardado en IndexedDB antes de mostrar “encolado”:
navigator.serviceWorker.register("/sw.js", { scope: "/" });
// sw.js
self.addEventListener("install", (event) => {
event.waitUntil(cacheAppShell());
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/reports/")) {
event.respondWith(networkFirstReport(event.request));
}
});
self.addEventListener("sync", (event) => {
if (event.tag === "retry-report-saves") {
event.waitUntil(flushIndexedDbQueue());
}
});Background Sync solo está disponible en algunos navegadores. Si el registro falla o la API no está presente, invoca el mismo procesador de cola cuando la página se inicie, vuelva a primer plano o reciba un evento online. Los eventos de conectividad son disparadores de reintentos, no pruebas de éxito; solo la respuesta del servidor confirma la finalización. Cada guardado almacena una clave de idempotencia, el número de intentos y la hora del siguiente reintento. Los conflictos pasan a un estado de revisión en lugar de sobrescribirse indefinidamente.
Paso 7: Gestionar el ciclo de vida y las actualizaciones del Service Worker
Un Service Worker controla las páginas solo tras la descarga, instalación, espera y activación. Tras el primer registro, el documento actual normalmente requiere una navegación adicional antes de quedar bajo control. Una nueva versión puede instalarse y quedar en espera de que se cierren las páginas que usan la versión antigua. skipWaiting() y clients.claim() pueden tomar el control más rápido, pero una página antigua vinculada a un nuevo worker puede fallar si los protocolos difieren. Versiona los esquemas de mensajes y de caché según corresponda.
El navegador puede terminar un worker entre eventos y reiniciarlo para el siguiente evento. event.waitUntil() extiende la vida útil asociada con la Promise del evento actual; no crea un proceso residente. Persiste la cola, el conteo de intentos y la clave de idempotencia. El procesamiento de sincronización debe poder reanudarse desde cualquier registro y ejecutar de forma segura la misma solicitud dos veces.
Los Service Workers requieren un contexto seguro. HTTPS es el requisito en producción, mientras que localhost es una excepción de desarrollo. El ámbito por defecto está restringido por la ruta del script. Verifica también la directiva CSP worker-src, el contenido de la caché sensible al usuario, el aislamiento en el cierre de sesión y el comportamiento de CORS y credenciales para solicitudes de origen cruzado.
Paso 8: Validar con una matriz de fallos
Prueba el Dedicated Worker con archivos de 1 MiB y 200 MiB, entradas mal formadas y cancelaciones. Registra las tareas largas del hilo principal, el retraso de entrada, el rendimiento del análisis, el pico de memoria y el tiempo de liberación tras la cancelación. Compara las variantes con clonación estructurada, transferibles y por fragmentos, y elige en función del cuello de botella medido.
Prueba SharedWorker con dos suscripciones simultáneas, cerrando la página líder, cerrando la última página, microcortes de red, errores en el worker y un navegador sin SharedWorker. Las comprobaciones de aceptación incluyen el conteo real de conexiones en el servidor, la ausencia de vacíos o duplicados inesperados de mensajes, la continuidad del cursor tras la reconexión y un fallback que preserve la correctitud.
Prueba Service Worker en la primera visita, revisitas controladas, recarga offline, expiración de caché, una actualización en espera, cierre forzado del navegador, sync duplicado, agotamiento de cuota de almacenamiento y ausencia de Background Sync. Utiliza claves de idempotencia en el endpoint de guardado para demostrar que los intentos de al menos una vez (at-least-once) no crean escrituras duplicadas. Verifica que los datos privados en caché no se crucen entre usuarios.
Ejemplo de respuesta de alto nivel
“Comenzaría analizando a quién pertenece la tarea, cuánto tiempo debe vivir y si actúa como intermediario de red. El CSV de 200 MiB sirve únicamente a la página actual y es intensivo en CPU, por lo que usaría un Dedicated Worker. El hilo principal gestiona la selección de archivos y la UI; el worker parsea en fragmentos, reporta el progreso y comprueba la cancelación entre fragmentos. La mensajería estándar usa clonación estructurada. Si existe un búfer completo de 200 MiB en ambos lados, el supuesto de la entrevista sitúa los bytes puros cerca de 400 MiB, por lo que evaluaría un ArrayBuffer transferible que mueva la propiedad o fragmentos por streaming con un número limitado en tránsito.
Mantendría el WebSocket único entre pestañas en un SharedWorker. Todas las páginas deben tener exactamente el mismo origen y vuelven a declarar sus suscripciones mediante su MessagePort. El worker almacena solo el estado de conexión y enrutamiento reconstruible. No es persistencia; el estado puede desaparecer tras cerrar la última página, cerrar el navegador o por un fallo del worker. Detectaría la característica. Si la conexión única es solo una optimización, el fallback es una conexión por pestaña. Si es una restricción estricta, usaría BroadcastChannel más elección basada en concesiones, con épocas e IDs de evento para split-brain y duplicados.
Usaría un Service Worker para el shell offline, el reporte más reciente y el guardado posterior. Intercepta solicitudes dentro de su ámbito: precaching versionado para el shell y network-first con fallback a caché para el reporte. El guardado se escribe en IndexedDB con una clave de idempotencia antes de registrar la sincronización en segundo plano. Sin Background Sync, el inicio, pasar a primer plano o la reconexión invocan el mismo procesador de cola. Dado que el Service Worker puede detenerse entre eventos, la cola nunca vive solo en una variable global; cada ejecución recupera el estado duradero y asocia el trabajo actual con waitUntil().
Finalmente, validaría las tareas largas y la memoria en el hilo principal, el conteo real de conexiones WebSocket y el comportamiento de reconexión, el reinicio offline y la sincronización duplicada, además de la espera de actualización del Service Worker, los fallbacks en navegadores antiguos, el aislamiento de caché por usuario y los límites de HTTPS y CSP”.
Errores comunes
- Usar un Service Worker como hilo de cómputo de larga duración → el navegador puede terminarlo entre eventos o durante trabajos prolongados → usa un Dedicated Worker para el cómputo de la página y reserva Service Worker para el trabajo de red guiado por eventos.
- Enviar un objeto de 200 MiB con
postMessagesin medir la memoria → la clonación estructurada y los resultados analizados pueden coexistir generando un gran pico de memoria → compara vías transferibles, fragmentadas y por streaming y mide el pico. - Asumir que cualquier worker garantiza una app fluida → la CPU, la recolección de basura y la serialización siguen consumiendo tiempo → mide las tareas largas del hilo principal, el rendimiento y el tamaño de lote de los mensajes.
- Tratar la memoria de SharedWorker como un estado confiable → puede desaparecer tras cerrarse la última referencia o al salir del navegador → conserva únicamente el estado de ejecución reconstruible y persiste los cursores y colas críticas.
- Ignorar los requisitos de mismo origen exacto → un esquema, host o puerto diferente no puede compartirlo → mapea los límites de origen antes de elegir la coordinación del lado del cliente.
- Copiar un sistema de elección complejo siempre que SharedWorker no esté disponible → el negocio puede necesitar correctitud, no exactamente una sola conexión → decide si la cantidad de conexiones es una restricción estricta y, de lo contrario, recurre a una conexión por pestaña.
- Mantener una cola offline en una variable global del Service Worker → un reinicio del worker pierde las tareas → escribe primero en IndexedDB e invoca un procesador de cola seguro frente a reintentos.
- Depender únicamente de Background Sync → todavía no es una característica Baseline soportada por todos los navegadores principales → reintenta también al inicio, al pasar a primer plano y al reconectarse.
- Forzar que cada actualización tome el control inmediatamente → una página antigua y un nuevo worker pueden tener protocolos incompatibles → versiona mensajes, cachés y migraciones antes de optar por omitir la espera.
- Almacenar en caché cada respuesta exitosa → los datos privados pueden persistir entre cuentas o reutilizarse incorrectamente → utiliza una lista blanca de URLs, aislamiento por usuario, limpieza al cerrar sesión y validación de respuestas.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿Qué cambia cuando el CSV crece a 2 GiB y la memoria móvil es insuficiente?
Deja de llamar a file.arrayBuffer() para toda la entrada. Lee fragmentos con Blob.stream() o slice(), conserva las filas parciales entre límites de decodificación y haz que el worker devuelva agregados o lotes columnares. Limita los fragmentos en tránsito y exige confirmaciones para controlar la contrapresión. Expón puntos de cancelación. Si el producto debe retener todas las filas, vuelca a IndexedDB o archivos segmentados y pagina los resultados en lugar de mantener el grafo de objetos completo en el heap de JavaScript. Agrega un límite específico por dispositivo y una vía de rechazo explícita.
Pregunta de seguimiento 2: ¿Qué sucede si páginas en diferentes subdominios deben compartir exactamente un WebSocket?
SharedWorker no puede cruzar límites de origen exactos. Una opción es un origen controlado que posea la conexión activa y exponga un puente de mensajes por iframe auditado, con validación estricta de targetOrigin, emisor y esquema. Otra opción es trasladar la coordinación de la conexión única al servidor mientras cada página mantiene una conexión de cliente independiente. La primera acopla seguridad y disponibilidad; la segunda consume más conexiones en el servidor. Elige en función de la restricción estricta real en lugar de intentar saltarte la política de mismo origen.
Pregunta de seguimiento 3: SharedWorker es Baseline 2026, ¿por qué mantener un fallback?
Utiliza la matriz de navegadores de tu producto. Baseline 2026 describe una capacidad recientemente estandarizada en las versiones actuales de los navegadores; no cubre todos los dispositivos antiguos, versiones empresariales congeladas, modos de privacidad o entornos embebidos. La detección de características es de bajo costo. Comienza con una conexión por pestaña como fallback e implementa elección de líder solo si una restricción estricta de conexiones lo justifica.
Pregunta de seguimiento 4: ¿Cómo evitas escrituras duplicadas cuando Background Sync repite un guardado?
El cliente genera una clave de idempotencia estable para la operación de negocio. La cola almacena al menos su payload, clave, conteo de intentos y hora del siguiente reintento. El servidor garantiza la unicidad o guarda el resultado por usuario y clave, devolviendo el resultado original ante un duplicado. El cliente elimina el elemento de la cola solo tras recibir confirmación. Un tiempo de espera agotado representa un resultado desconocido y genera un reintento con la misma clave, nunca con una nueva. Los conflictos de versión de negocio pasan a revisión en lugar de sobrescribirse silenciosamente.
Pregunta de seguimiento 5: Se instala un nuevo Service Worker, pero los usuarios mantienen una página antigua abierta indefinidamente. ¿Qué haces?
Notifica a las páginas controladas que hay una actualización lista y permite que el usuario recargue en un momento seguro. Un parche de seguridad puede usar skipWaiting() y clients.claim() solo cuando las páginas antiguas y nuevas sigan siendo compatibles con el protocolo de mensajes del worker y las migraciones de caché e IndexedDB sean reentrantes. Las pruebas de lanzamiento deben evaluar una página antigua con el nuevo worker. Cuando no se pueda garantizar la compatibilidad, mantén la fase de espera en lugar de optimizar únicamente para la activación inmediata.
Pregunta de seguimiento 6: ¿Por qué no usar un Service Worker para compartir también el WebSocket entre pestañas?
El ciclo de vida de un Service Worker está orientado a eventos, y el navegador puede detenerlo cuando esté inactivo. Un WebSocket requiere una conexión continua y una sesión en memoria, lo que entra en conflicto con ese modelo. Un SharedWorker es un mejor propietario mientras existan referencias de páginas abiertas. Si el navegador de destino carece de SharedWorker y una conexión única es obligatoria, utiliza elección entre pestañas o modifica la arquitectura del servidor en lugar de asumir que el Service Worker permanecerá residente.