Cache storage y el manifiesto de la aplicación web
El almacén de respuestas que usa el service worker, cómo se inspecciona y se purga desde el panel, y el panel de manifiesto con sus comprobaciones de instalabilidad.
Dos secciones del panel de aplicación cubren la parte de la plataforma que convierte una página en algo instalable y capaz de funcionar sin red: el almacén de respuestas, que guarda pares de petición y respuesta bajo tu control total, y el manifiesto, que declara cómo debe presentarse la aplicación cuando se instala. Las dos se inspeccionan aquí, y las dos tienen comportamientos que confunden si no se conoce su modelo.
- Inspeccionar el contenido del almacén de respuestas y borrar entradas concretas.
- Explicar en qué se diferencia de la caché HTTP y por qué eso importa.
- Leer el panel de manifiesto y sus avisos de instalabilidad.
- Manipular el almacén desde la consola para reproducir estados.
El almacén de respuestas
Es un almacén de objetos de respuesta indexados por objetos de petición, y su propiedad definitoria es que está completamente bajo control del código: nada entra ni sale si tú no lo pones o lo quitas. No obedece a Cache-Control, no caduca solo, no se invalida por sí mismo.
En el panel aparece cada caché con nombre, y dentro, la lista de entradas con la URL, el tipo de respuesta, el estado, el tamaño y las cabeceras. Al seleccionar una entrada se ve la respuesta completa, cabeceras incluidas.
Las acciones disponibles son eliminar una entrada, vaciar una caché y eliminar la caché entera. Todas son inmediatas, y son la herramienta principal para depurar un service worker que sirve contenido antiguo.
En qué se diferencia de la caché HTTP
| Aspecto | Caché HTTP | Almacén de respuestas |
|---|---|---|
| Quién decide qué entra | El navegador según cabeceras | Tu código, explícitamente |
| Caducidad | Automática por cabeceras | Ninguna, salvo que la implementes |
| Invalidación | Por cabeceras y validadores | Manual |
| Acceso desde código | Ninguno | Total |
| Cuándo se consulta | Automáticamente | Solo si tú la consultas |
| Ámbito | Particionada por sitio | Por origen |
La diferencia que más consecuencias tiene es la de caducidad. Una respuesta guardada aquí vive indefinidamente. Si tu estrategia guarda el documento HTML y lo sirve desde caché, ese documento se seguirá sirviendo hasta que algún código lo sustituya, independientemente de cuántas veces despliegues. Es el mecanismo exacto del bug que trata el nivel siguiente.
La ausencia de caducidad automática no es un defecto: es el punto. Este almacén existe para que puedas implementar la política que necesites, incluida la de funcionar sin red indefinidamente. La contrapartida es que la política de expulsión también es tuya, y una caché sin política de expulsión crece hasta agotar la cuota.
Manipularlo desde la consola
Todas las operaciones del panel se pueden hacer desde código, lo que permite reproducir estados y escribir comprobaciones.
// Inventario completo de las caches del origen, con tamaños
(async () => {
const nombres = await caches.keys();
if (!nombres.length) return console.log('No hay caches en este origen.');
const filas = [];
for (const nombre of nombres) {
const cache = await caches.open(nombre);
const peticiones = await cache.keys();
let bytes = 0;
for (const req of peticiones) {
const res = await cache.match(req);
if (!res) continue;
const buf = await res.clone().arrayBuffer().catch(() => null);
bytes += buf ? buf.byteLength : 0;
}
filas.push({ cache: nombre, entradas: peticiones.length, kb: Math.round(bytes / 1024) });
}
console.table(filas.sort((a, b) => b.kb - a.kb));
// Detalle de una cache concreta
window.verCache = async (nombre) => {
const cache = await caches.open(nombre);
const peticiones = await cache.keys();
const detalle = [];
for (const req of peticiones) {
const res = await cache.match(req);
detalle.push({
url: new URL(req.url).pathname.slice(-52),
metodo: req.method,
estado: res?.status,
tipo: res?.type,
contentType: res?.headers.get('content-type') || '',
fecha: res?.headers.get('date') || ''
});
}
console.table(detalle);
return detalle;
};
window.purgarTodo = async () => {
const n = await caches.keys();
await Promise.all(n.map(x => caches.delete(x)));
console.log('Eliminadas', n.length, 'caches. Recarga para comprobar.');
};
console.log('Usa verCache(nombre) o purgarTodo()');
})();
La columna de fecha de la respuesta es la que más veces cierra un diagnóstico: si el documento en caché tiene una fecha de hace tres semanas, ya sabes por qué el usuario ve la versión antigua.
Este patrón de expulsión por límite es el que falta en la mayoría de las implementaciones caseras:
// Politica de expulsion por numero de entradas, para dentro del service worker
async function limitarCache(nombre, maximo) {
const cache = await caches.open(nombre);
const claves = await cache.keys();
if (claves.length <= maximo) return 0;
// Las claves vienen en orden de insercion: las primeras son las mas antiguas
const sobrantes = claves.slice(0, claves.length - maximo);
await Promise.all(sobrantes.map(k => cache.delete(k)));
return sobrantes.length;
}
// Uso tras cada escritura:
// await cache.put(peticion, respuesta);
// await limitarCache('imagenes-v1', 60);
El detalle que lo hace funcionar es que las claves se devuelven en orden de inserción, lo que permite implementar una expulsión de las más antiguas sin guardar metadatos aparte.
El panel de manifiesto
El manifiesto es un fichero declarativo que describe cómo se presenta la aplicación cuando el usuario la instala: su nombre, sus iconos, su color, su orientación, su modo de presentación y su ruta de inicio.
El panel lo muestra ya interpretado, con secciones para la identidad, la presentación, los iconos, los accesos directos y las capturas. Tres cosas concretas que se comprueban aquí y en ningún otro sitio:
Los avisos de instalabilidad. El panel enumera lo que falta para que la aplicación sea instalable. Los requisitos habituales son un manifiesto válido enlazado desde el documento, un nombre, iconos de los tamaños requeridos, una ruta de inicio válida, un modo de presentación adecuado, y servirse por una conexión segura. La lista cambia entre versiones y por eso conviene leerla en el panel en lugar de fiarse de una lista escrita.
Los iconos renderizados. El panel los descarga y los muestra, lo que revela de inmediato los dos errores más comunes: una ruta rota que en producción da un icono genérico, y un icono sin la declaración de propósito adaptable, que en algunos sistemas se recorta mal.
El modo de presentación resultante. Ver declarado el modo permite anticipar que la aplicación instalada no tendrá barra de direcciones, y eso a su vez plantea la pregunta que casi nadie se hace hasta que es tarde: si tu interfaz no tiene navegación propia, la aplicación instalada será un callejón sin salida.
Un aviso sobre el desarrollo con manifiesto: el navegador cachea el manifiesto de forma agresiva, así que un cambio puede no reflejarse hasta que se fuerza una actualización. El panel tiene la acción correspondiente, y si aun así no se actualiza, desinstalar y volver a instalar la aplicación es el remedio seguro.
Al final de este nivel merece la pena poner los cinco mecanismos de persistencia en una sola escala, porque la pregunta práctica no es qué hace cada uno sino dónde guardar cada cosa, y el criterio que ordena la respuesta es cuánto control tienes sobre su ciclo de vida. En un extremo está la caché HTTP: no la controlas en absoluto, el navegador decide qué guarda y qué desaloja, y su única palanca son unas cabeceras que actúan sobre copias que ya no están en tu poder. Es una optimización oportunista y hay que tratarla como tal: nunca dependas de que algo esté ahí. Un escalón por encima están las cookies, que controlas al escribirlas y que después obedecen a reglas del navegador que no puedes cambiar, incluidas las de descarte por límite. Después el almacenamiento web, que sí controlas por completo mientras exista, pero que es síncrono, minúsculo y susceptible de desalojo. Después IndexedDB, con control total, tamaño amplio, acceso desde workers, y aun así sujeto a la política de desalojo del navegador. Y en el otro extremo el almacén de respuestas, que es el único que combina control total sobre el contenido, sobre el momento de la escritura, sobre la política de expulsión y sobre el momento de la lectura, porque nada se consulta si tu código no lo consulta. Esa combinación es lo que lo convierte en la base de cualquier estrategia seria de funcionamiento sin red, y también lo que lo hace peligroso: es el único almacén capaz de servir contenido antiguo a un usuario indefinidamente sin que ningún despliegue lo corrija, porque no hay ningún mecanismo automático que lo invalide. La regla que se deriva de toda la escala es que el control y la responsabilidad van juntos, y son la misma cosa vista desde los dos lados. La caché HTTP no puede romperte el despliegue porque tampoco te deja controlarla; el almacén de respuestas te deja hacer cualquier cosa y por eso puede romperte el despliegue durante meses. Elegir dónde guardar algo es, en realidad, elegir cuánta responsabilidad de invalidación estás dispuesto a asumir, y esa decisión hay que tomarla a conciencia y no por la comodidad de la API.