Lección 12 de 1514 min
Red, almacenamiento y segundo plano
Fetch y XMLHttpRequest, Web Storage, IndexedDB y Cache API, Service Workers, Web Workers, Broadcast Channel y Web Locks.
Imagina una app de notas que usas en el metro. Al abrirla sin cobertura carga igual; las notas que escribes se guardan en el dispositivo y se envían solas cuando vuelve la conexión; si la tienes abierta en dos pestañas, las dos muestran lo mismo; y al exportar cien notas, la interfaz no se congela.
Todo eso lo resuelven APIs del navegador: unas hablan con la red, otras guardan datos en el dispositivo, otras trabajan en segundo plano y otras coordinan varias pestañas. En esta lección verás qué hace cada una y cuándo elegirla.
Pedir datos a la red
Fetch API
La forma moderna de hacer peticiones HTTP desde JavaScript, con promesas.
- Basada en:
- promesas
- Mantiene:
- WHATWG
fetch(url, opciones) envía una petición y devuelve una promesa con un objeto Response. En las opciones eliges el método, las cabeceras y el cuerpo, y con respuesta.json(), .text() o .blob() lees el contenido. Está disponible en la página, en los workers y también en Node.js.
Para cancelar una petición, o ponerle un tiempo máximo, le pasas una señal: la de un AbortController o AbortSignal.timeout(5000). Y como respuesta.body es un stream, puedes procesar una respuesta grande a medida que llega.
Úsalo cuando
Para cualquier petición nueva que escribas en el navegador: es el estándar, y muchos clientes HTTP de los frameworks se apoyan en él.
Ten en cuenta
fetch no falla con un 404 o un 500: la promesa solo se rechaza si no hay red o si la cancelas. Comprueba siempre respuesta.ok. Y las peticiones a otros dominios están sujetas a CORS, que verás en el módulo de seguridad web.
async function cargarPedidos() {
const respuesta = await fetch("/api/pedidos", {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5000), // cancela si tarda más de 5 s
});
if (!respuesta.ok) {
throw new Error(`Error ${respuesta.status} al cargar los pedidos`);
}
return respuesta.json();
}XMLHttpRequest
La API veterana de peticiones, basada en eventos: la que hizo posible AJAX.
- Basada en:
- eventos
- Desde:
- 1999
XMLHttpRequest (XHR) nació a finales de los 90 en Internet Explorer y permitió algo entonces revolucionario: pedir datos sin recargar la página, la técnica que se acabó llamando AJAX. A pesar del nombre, sirve para cualquier formato, no solo XML. Funciona con eventos (load, error, progress) en lugar de promesas.
Hoy fetch la sustituye en casi todo, pero XHR conserva una ventaja: el evento progress de xhr.upload, que informa de cómo va una subida. Por eso algunas librerías siguen usándola por debajo: Axios, por ejemplo, la usa por defecto en el navegador, y el HttpClient de Angular puede usar XHR o fetch (con withFetch()).
Úsalo cuando
Cuando necesitas mostrar el progreso de subida de un archivo, o al mantener código antiguo.
Ten en cuenta
No la uses en modo síncrono (pasando false como tercer argumento de open()): congela la página y los navegadores lo desaconsejan. Para todo lo demás, prefiere fetch.
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/fotos");
xhr.upload.addEventListener("progress", (e) => {
barra.value = Math.round((e.loaded / e.total) * 100);
});
xhr.addEventListener("load", () => {
if (xhr.status === 201) mostrarAviso("Foto subida");
});
xhr.send(new FormData(formulario));Guardar datos en el navegador
Todos estos almacenes son por origen (protocolo, dominio y puerto): https://tienda.ejemplo no puede leer lo que guardó https://blog.ejemplo. Y los datos están en el dispositivo del usuario: él puede borrarlos cuando quiera, y el navegador también, si se queda sin espacio.
Web Storage: localStorage y sessionStorage
Pares clave-valor de texto, con una API síncrona muy sencilla.
- API:
- síncrona
- Guarda:
- texto
localStorage guarda pares clave-valor que sobreviven al cerrar el navegador; sessionStorage hace lo mismo, pero solo para esa pestaña y hasta que se cierra. La API cabe en una línea: setItem(), getItem(), removeItem() y clear().
Solo guarda texto: para un objeto, conviértelo con JSON.stringify() y recupéralo con JSON.parse(). Cuando una pestaña cambia localStorage, las demás pestañas del mismo origen reciben el evento storage, lo que permite sincronizar cosas sencillas.
Úsalo cuando
Para datos pequeños y no sensibles: el tema elegido, el idioma, si ya cerraste un aviso o el borrador de un comentario.
Ten en cuenta
Es síncrona: cada lectura y escritura bloquea el hilo principal. Tiene poca capacidad (unos pocos megabytes), no existe en los workers y cualquier script de la página puede leerla: no guardes ahí tokens de sesión ni datos personales.
// Guardar: solo admite texto
const preferencias = { tema: "oscuro", idioma: "es" };
localStorage.setItem("preferencias", JSON.stringify(preferencias));
// Leer (getItem devuelve null si no existe)
const guardadas = JSON.parse(localStorage.getItem("preferencias") ?? "{}");
// Dura mientras la pestaña siga abierta
sessionStorage.setItem("paso-del-formulario", "3");IndexedDB
Una base de datos dentro del navegador para muchos datos estructurados.
- API:
- asíncrona
- Guarda:
- objetos y archivos
IndexedDB es una base de datos asíncrona y transaccional. Organizas los datos en almacenes de objetos (object stores), parecidos a tablas, que guardan objetos de JavaScript tal cual (también fechas, Blob y archivos), y creas índices para buscarlos rápido por un campo.
Admite mucho más espacio que Web Storage, una parte del disco libre que decide cada navegador, y funciona también en los workers. Su API original, basada en eventos, es farragosa, así que casi todo el mundo usa una capa encima, como la librería idb, que la convierte en promesas, o Dexie.
Úsalo cuando
Para apps que funcionan sin conexión: guardar el catálogo, las notas o una cola de cambios pendientes de enviar al servidor.
Ten en cuenta
Los cambios de estructura (nuevos almacenes o índices) solo se pueden hacer al abrir la base de datos con un número de versión mayor. Y los datos no son eternos: el navegador puede borrarlos si falta espacio, salvo que pidas almacenamiento persistente con navigator.storage.persist().
import { openDB } from "idb";
const db = await openDB("mis-notas", 1, {
upgrade(db) {
const notas = db.createObjectStore("notas", { keyPath: "id" });
notas.createIndex("por-fecha", "actualizada");
},
});
await db.put("notas", {
id: crypto.randomUUID(),
texto: "Comprar pan",
actualizada: new Date(),
});
const ordenadas = await db.getAllFromIndex("notas", "por-fecha");Cache API
Un almacén de pares petición-respuesta, pensado para funcionar sin conexión.
- Guarda:
- peticiones y respuestas
La Cache API guarda respuestas HTTP completas asociadas a la petición que las produjo. Abres una caché con nombre (caches.open("tienda-v2")), añades respuestas con add(), addAll() o put() y las buscas con match(). Todo con promesas.
Está disponible en la página y en los workers, pero brilla junto a un Service Worker, que intercepta cada petición y decide si responde desde la caché o desde la red. No tiene nada que ver con la caché HTTP del navegador, la que controlan cabeceras como Cache-Control: aquí mandas tú.
Úsalo cuando
Para guardar los archivos de la app (HTML, CSS, JavaScript, iconos) y que cargue sin red, o respuestas de la API que quieras mostrar aunque no haya conexión.
Ten en cuenta
Lo que guardas no caduca solo: si no borras las cachés antiguas al publicar una versión nueva, tus usuarios pueden seguir viendo la anterior y acumular datos. Además, solo funciona en contextos seguros (HTTPS).
const cache = await caches.open("tienda-v2");
// Descarga y guarda los archivos básicos de la app
await cache.addAll(["/", "/estilos.css", "/app.js", "/logo.svg"]);
// Más tarde, aunque no haya red: la respuesta guardada
const respuesta = await cache.match("/estilos.css");
// Al publicar una versión nueva, borra la caché anterior
await caches.delete("tienda-v1");Trabajar en segundo plano
El código de tu página se ejecuta en un solo hilo, el mismo que pinta la interfaz y responde a los clics. Si lo ocupas con una tarea larga, la página se congela. Los workers son scripts que se ejecutan en otros hilos, sin acceso al DOM, y se comunican con la página mediante mensajes.
Service Workers
Un proxy programable entre tu app y la red: la base de las apps que funcionan offline.
- Requiere:
- HTTPS
- Acceso al DOM:
- no
Un Service Worker es un script que el navegador instala para tu sitio y que se interpone entre la página y la red: cada petición pasa por su evento fetch, donde decides si respondes desde la caché, desde la red o con una mezcla de ambas. No depende de que la pestaña siga abierta: el navegador lo despierta cuando hace falta y lo detiene cuando no.
Tiene un ciclo de vida propio: se registra desde la página, se instala (buen momento para precargar archivos en la Cache API), se activa (para limpiar cachés antiguas) y, desde entonces, controla las páginas de su ámbito. También es quien recibe las notificaciones push. Es la base de las PWA (aplicaciones web progresivas), y herramientas como Workbox o @angular/service-worker lo generan por ti.
Úsalo cuando
Para que tu app funcione sin conexión o con mala red, cargue al instante en las visitas repetidas o reciba notificaciones push.
Ten en cuenta
Solo funciona en HTTPS (o localhost). Una versión nueva no toma el control hasta que se cierran todas las pestañas que usan la anterior, salvo que lo fuerces con skipWaiting(). Y un error en la estrategia de caché puede dejar a tus usuarios atrapados en una versión vieja: pruébala bien.
// En la página: navigator.serviceWorker.register("/sw.js");
self.addEventListener("install", (evento) => {
evento.waitUntil(
caches.open("tienda-v2").then((cache) => cache.addAll(["/", "/app.js"])),
);
});
// Primero la caché; si no está ahí, la red
self.addEventListener("fetch", (evento) => {
evento.respondWith(
caches.match(evento.request).then((guardada) => guardada ?? fetch(evento.request)),
);
});Web Workers
Ejecuta código pesado en otro hilo para que la interfaz no se congele.
- Acceso al DOM:
- no
- Comunicación:
- postMessage()
Un Web Worker es un script que corre en paralelo con la página. Lo creas con new Worker(url) y os comunicáis con postMessage() y el evento message. Los datos se copian de un lado a otro, no se comparten, así que ninguno puede estropear el estado del otro.
Dentro de un worker tienes fetch, IndexedDB, temporizadores y casi todo JavaScript, pero no el DOM: no puede tocar la página. A diferencia del Service Worker, vive y muere con la página que lo creó y no intercepta peticiones.
Úsalo cuando
Para tareas de cálculo que duran más de unas décimas de segundo: procesar una imagen, comprimir archivos, analizar un CSV enorme, buscar entre miles de registros o ejecutar código WebAssembly.
Ten en cuenta
Copiar datos grandes también cuesta: los buffers binarios puedes pasarlos como transferibles (postMessage(datos, [datos.buffer])), que cambian de dueño sin copiarse. Y no lo uses para tareas cortas: crear un worker y enviarle mensajes tiene su propio coste.
// exportar.js (en la página)
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
worker.postMessage({ notas });
worker.addEventListener("message", (e) => descargar(e.data));
// worker.js (en otro hilo: aquí no hay DOM)
self.addEventListener("message", (e) => {
const csv = e.data.notas.map((n) => `"${n.texto}",${n.fecha}`).join("\n");
self.postMessage(new Blob([csv], { type: "text/csv" }));
});Nota
No confundas los dos workers: el Service Worker se sitúa entre tu app y la red y sigue existiendo cuando cierras la pestaña; el Web Worker es un ayudante de cálculo de una página concreta.
Coordinar pestañas y workers
Un usuario puede tener tu app abierta en varias pestañas, cada una con sus propios workers. Estas dos APIs les permiten hablar entre sí y no pisarse.
Broadcast Channel API
Un canal de mensajes entre las pestañas, ventanas y workers del mismo origen.
- Alcance:
- mismo origen
Cada contexto que quiera participar crea un canal con el mismo nombre (new BroadcastChannel("sesion")). Lo que uno envía con postMessage() les llega a todos los demás que tengan abierto ese canal, como en una emisora de radio; al emisor no le llega su propio mensaje.
Es más directo que usar el evento storage de localStorage para avisar a otras pestañas: envía cualquier dato que se pueda clonar (objetos, fechas, archivos), no solo texto, y no deja nada guardado.
Úsalo cuando
Para cerrar la sesión en todas las pestañas a la vez, actualizar el carrito en todas cuando cambia en una o avisar de que hay una versión nueva.
Ten en cuenta
Solo funciona dentro del mismo origen y del mismo navegador: no comunica dispositivos distintos ni otros usuarios; para eso necesitas un servidor y, por ejemplo, WebSocket. Los mensajes no se guardan: si una pestaña no está escuchando en ese momento, se los pierde.
const canal = new BroadcastChannel("sesion");
// En la pestaña donde el usuario pulsa «Salir»
function cerrarSesion() {
borrarDatosLocales();
canal.postMessage({ tipo: "logout" });
location.assign("/entrar");
}
// En todas las demás pestañas
canal.addEventListener("message", (e) => {
if (e.data.tipo === "logout") location.assign("/entrar");
});Web Locks API
Cerrojos para que solo una pestaña o worker haga algo a la vez.
- Modos:
- exclusive, shared
Con navigator.locks.request(nombre, función) pides un cerrojo con nombre. Si nadie lo tiene, tu función se ejecuta; si lo tiene otra pestaña o worker del mismo origen, la tuya espera su turno. El cerrojo se libera solo cuando termina la promesa que devuelve la función, aunque termine con un error.
Por defecto es exclusivo, pero también puedes pedirlo en modo shared (varios lectores a la vez) o con ifAvailable: true, para no esperar y saltarte la tarea si otro ya la está haciendo.
Úsalo cuando
Cuando varias pestañas podrían hacer a la vez algo que debe ocurrir una sola vez: sincronizar los cambios pendientes con el servidor, renovar un token o migrar los datos de IndexedDB.
Ten en cuenta
Si la función nunca termina, el cerrojo no se libera y las demás pestañas esperan para siempre: asegúrate de que siempre acaba. Y solo coordina contextos del mismo origen en el mismo navegador, no servidores ni dispositivos.
// Aunque haya cinco pestañas abiertas, solo una sincroniza a la vez
async function sincronizar() {
await navigator.locks.request("sincronizar-notas", { ifAvailable: true }, async (cerrojo) => {
if (!cerrojo) return; // otra pestaña ya lo está haciendo
const pendientes = await leerPendientes(); // desde IndexedDB
await fetch("/api/notas", { method: "POST", body: JSON.stringify(pendientes) });
await marcarComoEnviadas(pendientes);
}); // al terminar la función, el cerrojo se libera
}Interactivo
¿Dónde guardo los datos en el navegador?
Las tres opciones son por origen y viven en el dispositivo del usuario. Elige según qué guardas y cuánto.
| Característica | Web Storage | IndexedDB | Cache API |
|---|---|---|---|
| Qué guarda | Pares clave-valor de texto | Objetos, fechas y archivos, con índices | Pares petición-respuesta HTTP |
| API | Síncrona y muy simple | Asíncrona y transaccional (más cómoda con idb) | Asíncrona, con promesas |
| Capacidad | Pequeña: unos pocos MB por origen | Grande: una parte del disco libre | Grande: comparte cuota con IndexedDB |
| En workers | No | Sí | Sí, sobre todo en el Service Worker |
| Uso típico | Preferencias, avisos cerrados, borradores pequeños | Datos de la app sin conexión, colas de cambios | Archivos de la app y respuestas para usar sin red |
| Cuánto dura | localStorage, hasta que se borre; sessionStorage, hasta cerrar la pestaña | Hasta que se borre o el navegador necesite espacio | Hasta que la borres tú o el navegador necesite espacio |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Quieres recordar si el usuario eligió el tema oscuro para aplicarlo nada más cargar la página.
Para recordar
- Usa
fetchpara tus peticiones y comprueba siemprerespuesta.ok; XMLHttpRequest solo aporta el progreso de subida. - Web Storage para poco texto; IndexedDB para muchos datos estructurados; Cache API para respuestas HTTP y los archivos de la app.
- Un Service Worker es un proxy entre tu app y la red, la base del modo sin conexión; un Web Worker es un hilo extra para cálculos pesados.
- Broadcast Channel comunica pestañas del mismo origen, y Web Locks evita que hagan lo mismo a la vez.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Llamas a
fetch("/api/pedidos")y el servidor responde404. ¿Qué pasa con la promesa?2.¿Por qué no conviene guardar miles de registros en
localStorage?3.¿Cuál es la diferencia principal entre un Service Worker y un Web Worker?
4.Tu Service Worker guarda
app.jsen la Cache API. Publicas una versión nueva, pero algunos usuarios siguen viendo la antigua. ¿Qué es lo más probable?5.Dos pestañas de tu app sincronizan a la vez las notas pendientes y aparecen duplicadas en el servidor. ¿Qué API lo evita mejor?