Me llega un caso que parece trivial y no lo es. Una página en siteb.com embebe un iframe de sitea.com. Los dos, el iframe y la página padre, piden exactamente la misma imagen: https://imageurl.com/myImage.jpg. Primero la pide el iframe, después la pide el padre. La pregunta es sencilla: ¿la segunda petición se sirve desde caché?
La respuesta correcta es “depende del navegador”, y esa es justo la parte interesante. En Chrome se descarga dos veces. En Firefox y Safari, una sola vez. Mismo HTML, mismas cabeceras, mismo servidor, resultado distinto.
No es un bug de nadie. Es una decisión de diseño distinta sobre cómo se indexa la caché, y merece la pena entenderla porque afecta a cosas que damos por hechas todos los días.
Por qué la caché dejó de ser compartida
Durante años la caché HTTP se indexaba por un solo identificador: la URL del recurso. Si cdn.example/jquery.js ya estaba en disco, daba igual qué sitio lo pidiera; se servía de disco. Sobre eso se sostenía el argumento de los CDN públicos: cuantos más sitios sirvieran jQuery desde la misma URL, más probable era que quien navegara ya lo tuviera descargado.
El problema es que una caché global es un canal lateral perfecto. Si puedo medir cuánto tarda en resolverse un recurso, puedo deducir si ya estaba en el disco de quien navega, y de ahí deducir dónde ha estado. Los ataques concretos que documentó el equipo de Chrome son tres:
- Detección de historial. Cargo un recurso que solo existe en un sitio muy específico y mido el tiempo. Si viene de caché, quien navega ha estado ahí.
- Cross-site search. Variante más fina: compruebo si un recurso concreto está cacheado para inferir si una cadena de búsqueda aparece en los resultados privados de alguien.
- Supercookies. La caché como identificador persistente. Escribo un recurso con un identificador único y lo leo desde cualquier otro sitio; sobrevive a borrar cookies.
La respuesta de los navegadores fue particionar: la caché deja de ser una tabla global y pasa a ser una colección de compartimentos aislados. Safari lo hizo primero, sobre 2013. Chrome lo desplegó a partir de la versión 86, a finales de 2020. Firefox lo activó por defecto en la 85, en enero de 2021.
Hasta aquí, los tres coinciden. Donde se diferencian es en cuántos componentes lleva la clave.
La cache key, desglosada
Aquí está toda la diferencia. Chrome usa una clave triple:
Chrome, desde la v86
{ top-level site, frame site, URL del recurso }
Firefox y Safari usan una clave doble:
Firefox y Safari
{ top-level site, URL del recurso }
En ambos casos, “site” significa scheme://eTLD+1. El subdominio y el puerto no entran en el cálculo, así que https://a.example y https://www.a.example caen en la misma partición.
Apliquémoslo al caso. La petición del iframe:
Chrome (https://siteb.com, https://sitea.com, https://imageurl.com/myImage.jpg)
Firefox/Safari (https://siteb.com, https://imageurl.com/myImage.jpg)
Y la del padre, la misma URL desde el documento top-level:
Chrome (https://siteb.com, https://siteb.com, https://imageurl.com/myImage.jpg)
Firefox/Safari (https://siteb.com, https://imageurl.com/myImage.jpg)
Analicemos en detalle. En Firefox y Safari las dos claves son idénticas carácter a carácter, así que la segunda petición es un hit y el byte se sirve de disco. En Chrome el segundo componente cambia, sitea.com frente a siteb.com, así que son dos particiones distintas y la imagen se descarga dos veces dentro de la misma carga de página.
En la demo podemos pedir la imagen desde cada contexto y ver la clave que sale, qué particiones se van creando y cuántos bytes acaban viajando por la red. Cambiemos de navegador para comparar. Es una simulación: no se hace ninguna petición real ni se toca la caché del navegador, el resultado se calcula aplicando el modelo de cache key documentado de cada motor.
Por qué Chrome aísla más
La clave doble asume que quien publica la página top-level se fía de los frames que embebe. Es una suposición razonable en muchos casos y falsa en unos cuantos: un iframe de un tercero es código de otra persona ejecutándose dentro de nuestra página.
Con clave doble, ese iframe de un tercero comparte partición de caché con nuestro documento y con todos los demás frames de la página. Eso reabre una versión reducida del ataque original: en lugar de espiar entre sitios, se espía entre frames del mismo sitio. Un anuncio embebido puede sondear qué ha cacheado el documento que lo contiene.
Chrome metió el frame site en la clave precisamente para cerrar eso. En sus propios experimentos la diferencia de rendimiento entre clave doble y triple resultó pequeña, así que se quedaron con el aislamiento extra. Firefox y Safari optaron por el modelo simple: menos aislamiento entre frame y top-level, mejor tasa de aciertos en este caso concreto.
Ninguna de las dos posturas es gratis. Chrome paga cache misses; Firefox y Safari pagan superficie de ataque entre frames. Vale la pena tener claro que es un intercambio consciente y no un descuido.
Cadenas de iframes anidados
El caso se complica cuando hay más de un nivel: a.example embebe b.example, que a su vez embebe c.example, y es c.example quien pide el recurso.
La intuición dice que la clave debería llevar toda la cadena. No es así. Chrome usa el top-frame y el frame inmediato que hace la petición, y se salta lo que hay en medio:
Chrome: a.example > b.example > c.example, pide c.example
(https://a.example, https://c.example, https://x.example/lib.js)
↑ b.example no aparece por ningún lado
Esto tiene una consecuencia práctica poco obvia: dos cadenas de anidamiento completamente distintas comparten partición mientras coincidan los extremos. Y al revés, mover un recurso un nivel arriba o abajo en la jerarquía de frames cambia la partición e invalida la caché.
Probemos a pedir lib.js desde cada nivel de la cadena y veamos qué entra y qué se ignora:
Qué se rompe en el día a día
Esto no es teoría de navegadores. Son cuatro suposiciones que arrastramos en proyectos reales:
- Google Fonts y las fuentes de terceros. La idea de “si ya lo tiene cacheado de otro sitio, carga instantáneo” lleva años sin ser cierta entre sitios. Y dentro de una misma página, en Chrome, si la fuente la pide el documento y también un widget embebido en iframe, se descarga dos veces. El dato de Chrome es claro: los fallos de caché en fuentes de terceros pasaron del 21% al 28%, un 33% más.
- jQuery y librerías desde CDN compartido. El argumento clásico de usar
code.jquery.comen lugar de servir el fichero desde nuestro dominio era la caché compartida. Ese argumento ya no existe. Lo que queda es una conexión más que abrir, un punto de fallo más y un tercero al que dar acceso a nuestra ejecución. - Ad-servers y widgets embebidos. Un banner servido dentro de un iframe de un ad-server no comparte caché con nuestro documento en Chrome. Si la misma imagen o el mismo script aparecen en los dos sitios, se descargan los dos. Es el caso exacto del principio, y suele pasar desapercibido porque en el waterfall parecen dos peticiones a URLs distintas hasta que uno se fija.
- Pixels de analítica y tag managers en frames. Mismo patrón. El SDK que carga el frame no reutiliza nada de lo que ya cargó el documento contenedor.
El segundo caso es el que más veces sigo encontrando, y el que menos cuesta solucionar:
❌ Bad: apoyarse en una caché compartida que ya no existe
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
✅ Good: servir desde nuestro propio origen
<script src="/vendor/jquery-3.7.1.min.js"></script>
Chrome midió el coste agregado al desplegar el particionado. La tasa global de fallos de caché subió del 54% al 56%, un 3,6% más, y la fracción de bytes que vienen de red pasó del 75% al 78%. El impacto en First Contentful Paint quedó por debajo del 1% en la mayoría de cuantiles, con una regresión típica de en torno al 0,3%. Poco en la media, y bastante más en los casos concretos que dependían de compartir.
Compartir partición no basta: la eviction
Aquí conviene frenar, porque es el error que más veces he visto al depurar esto. Que la clave coincida no garantiza el hit. Compartir partición es condición necesaria, no suficiente.
En el caso de Firefox y Safari, las dos peticiones caen en la misma partición. Perfecto. Pero si la entrada ya no está en disco cuando llega la segunda petición, se vuelve a descargar igual. Y la entrada puede haber desaparecido por razones que no tienen absolutamente nada que ver con el particionado:
- LRU o LFU cuando la caché está llena. El navegador tiene un presupuesto de disco finito. Cuando se llena, descarta entradas por antigüedad o por frecuencia de uso. Nadie nos avisa.
- Expiración por cabeceras HTTP. Un
Cache-Control: max-agecorto o unExpiresya vencido hacen que la entrada deje de ser fresca. Esto no depende del particionado: es la capa de caducidad de HTTP y opera igual en los tres navegadores. - Presión de cuota del perfil. El navegador gestiona un presupuesto global de disco por perfil. Si el sistema va justo de espacio, o si otro origen ha estado escribiendo mucho, la caché se vacía entera sin preguntar.
Particionado y eviction son dos causas de MISS completamente independientes, y en el waterfall se ven exactamente igual. Distinguirlas importa porque llevan a soluciones distintas: una se afronta con cabeceras y con presupuesto de bytes, y sobre la otra no podemos hacer nada desde nuestro código.
Podemos verlo en la demo de arriba con el control Entre peticiones: elegimos Firefox o Safari, pedimos la imagen desde el iframe, descartamos la entrada y volvemos a pedirla desde el padre. La clave es idéntica, la partición es la misma, y aun así hay MISS.
El particionado no solo divide, también aprieta
Y aquí las dos causas se cruzan, que es la parte poco obvia.
Cada partición se comporta como una caché separada con su propio espacio dentro del mismo disco. Un recurso que antes ocupaba una entrada ahora ocupa N, una por cada combinación de sitios desde la que se pide. La misma fuente de Google Fonts puede tener una copia por cada sitio que visitamos, y en Chrome incluso varias copias dentro de un mismo sitio si la piden frames distintos.
El presupuesto de disco, en cambio, no se multiplicó. Así que más entradas compiten por el mismo espacio, la caché se llena antes y el LRU empieza a descartar entradas mucho antes. El particionado sube la tasa de fallos por dos vías a la vez: claves que ya no coinciden, y entradas que sobreviven menos tiempo porque hay más presión.
Merece la pena ser preciso con los números: los datos que publicó Chrome, ese 3,6% más de fallos y ese 4% más de bytes desde red, son la cifra agregada del despliegue. Recogen el efecto combinado de ambos mecanismos y no vienen desglosados por causa. Cuando leemos “el particionado cuesta un 3-4%”, lo que estamos leyendo incluye la duplicación de copias, no solo el cambio de clave.
Una nota para reproducirlo en DevTools
Si queremos reproducir el caso del iframe y el padre de forma fiable, dos avisos:
- Desactivar “Disable cache” no basta. Con esa casilla marcada todo se ve como red y el experimento no dice nada. Pero desmarcarla tampoco garantiza ver el hit, porque lo que hay que comprobar es si la entrada sigue viva entre las dos peticiones. La señal está en la columna
Sizedel panel Network:(disk cache)frente a un peso real transferido. - El intervalo entre ambas peticiones importa. Si medimos las dos dentro de la misma carga de página, cualquier MISS apunta al particionado. Si entre medias hay una recarga, o pasan minutos de sesión, o quien navega ha seguido navegando, sube la probabilidad de que la entrada se haya descartado sin más. Estaríamos atribuyendo al particionado algo que es LRU corriente.
En la práctica: las dos peticiones en la misma carga de página. Entre ellas pasan milisegundos, no hay margen para que nada se descarte, y cualquier MISS que veamos solo puede venir de la clave.
Cómo comprobarlo en nuestra propia web
Dos formas, una rápida y una detallada:
- La rápida: el panel Network. Cargamos la página, buscamos el recurso sospechoso y miramos la columna
Size. Si aparece dos veces, una con el peso real transferido y otra también con peso real en lugar de(disk cache), ahí está el doble fetch. Conviene activar la columnaInitiatorpara distinguir cuál sale del documento y cuál del iframe. Es el mismo gesto que hago en cualquier auditoría antes de mirar nada más. - La detallada:
chrome://net-export/. Grabamos un log de red mientras cargamos la página, lo abrimos en netlog-viewer y buscamosSplitCacheByNetworkIsolationKey. El sufijo nos dice en qué grupo estamos:Experiment_significa particionado activo,Control_oDefault_significan desactivado. Desde la v86 lo normal es lo primero, así que esto sirve sobre todo para confirmar que no hay una flag o una política de empresa cambiando el comportamiento por debajo.
Con las cautelas de eviction que acabamos de ver: caché activada, recarga normal en lugar de Cmd+Shift+R, y las dos peticiones dentro de la misma carga de página.
Cross-Origin Storage: recuperar lo compartido sin reabrir el problema
El particionado resolvió un problema de privacidad real y, de paso, dejó otro abierto: hay ficheros que de verdad son idénticos en miles de sitios y que hoy se descargan una y otra vez. Un modelo de IA de 1,35 GB. El canvaskit.wasm de Flutter. Una fuente de emojis con cobertura Unicode completa. React entero.
Cross-Origin Storage (explainer en la WICG) aborda esa raíz desde otro ángulo. En lugar de intentar relajar el particionado de la caché HTTP, propone un almacén distinto y paralelo: una caché content-addressable, donde la clave no es la URL sino el hash SHA-256 del contenido.
Ese cambio de clave es lo que lo hace posible. Si dos sitios necesitan exactamente los mismos bytes, llegan al mismo hash, aunque los sirvan desde URLs distintas. El fichero se guarda una vez y se comparte.
La API imperativa es corta:
// ✅ Good: recuperar de COS con fallback a red
const url = "https://cdn.example/model.bin";
const hash = {
algorithm: "SHA-256",
value: "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4",
};
let blob;
try {
const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
blob = await handle.getFile();
// Already in the shared store: zero bytes over the network.
} catch (err) {
if (err.name !== "NotFoundError") throw err;
blob = await fetch(url).then(r => r.blob());
}
Y guardar, declarando quién puede leerlo. Encadenado al bloque anterior, esto es lo que convierte el fallback en una descarga que solo ocurre una vez:
const handle = await navigator.crossOriginStorage.requestFileHandle(hash, {
create: true,
origins: "*",
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
Cómo evita reabrir el problema que motivó el particionado
Esta es la parte que hay que mirar con lupa, porque una caché compartida entre orígenes es exactamente lo que los navegadores desmontaron. La propuesta apila tres defensas:
- El campo
originsdecide la visibilidad. Si se omite, el fichero solo lo pueden recuperar orígenes del mismo site. Se puede dar una lista explícita, o poner'*'para hacerlo global. La visibilidad se puede ampliar después, nunca reducir. - La Public Hash List (PHL) con criterio de k-anonimato. El k-anonimato es la propiedad de que un dato solo se revele si hay al menos otros
kcasos indistinguibles de él, de forma que revelarlo no señale a ninguno en concreto. Aplicado aquí: para los ficheros marcados como'*', el navegador no confirma sin más que los tiene. Consulta una lista de hashes verificados: solo entran los que aparecen, byte a byte idénticos, en un número suficiente de orígenes distintos. Si el hash no está en esa lista, la respuesta esNotFoundErroraunque el fichero esté en disco. Esto corta de raíz el sondeo: no sirve de nada preguntar por un hash raro y único, porque nunca va a estar en la lista. - GREASE’ing. Aun estando todo en orden, el navegador puede devolver falsos negativos de vez en cuando. Un
NotFoundErrordeja de ser una señal fiable, lo que hace que sondear en volumen sea poco práctico. La propuesta pone un límite explícito: esto no debe hacerse con ficheros tan grandes que volver a descargarlos cueste más de lo que se gana en privacidad. Forzar la redescarga de un modelo de 1,35 GB para meter ruido no compensa.
No hay tamaño mínimo de fichero, y la propuesta razona por qué: sería trivial inflar cualquier fichero con relleno hasta pasar el umbral.
Qué soluciona, y qué no, respecto a la eviction
Conviene no presentar COS como almacenamiento permanente, porque no lo es. Sigue siendo una caché y sus entradas se pueden ir:
- El navegador podría borrar ficheros automáticamente. El explainer lo deja como opción abierta y pone como ejemplo un criterio tipo LRU, sin exigir ninguno.
- Hay cuota por origen que escribe: si guardar un fichero hiciera que el origen se pasara de su límite, la promesa se rechaza con
QuotaExceededError. Es la defensa contra el cache flooding, para que ningún sitio pueda llenar el almacén a propósito y expulsar los recursos de los demás. - La parte más firme es el control de quien navega: la propuesta espera que el navegador ofrezca una pantalla de configuración para ver qué ficheros hay en COS y qué orígenes han accedido a cada uno, con la opción de borrarlos. Al borrar los datos de un sitio se elimina su información de uso, y un fichero que se queda sin ningún origen que lo use puede desaparecer.
Lo que sí elimina COS es la duplicación. Al indexar por hash, el mismo fichero deja de tener N copias, una por partición, y pasa a tener una. Eso baja la presión de eviction global, que era la segunda de las dos vías por las que el particionado sube los fallos de caché. No la elimina, la alivia: menos entradas compitiendo por el mismo disco significa que lo que hay dentro sobrevive más tiempo.
Las cuatro formas de integrarlo
Además de la API imperativa, la propuesta define tres integraciones declarativas, todas apoyadas en integrity para que el hash sea verificable:
<!-- Solo same-site -->
<script src="framework.js" integrity="sha256-abc…" crossoriginstorage></script>
<!-- Global -->
<script src="lib.js" integrity="sha256-def…" crossoriginstorage="*"></script>
<!-- Lista explícita de orígenes -->
<link
rel="stylesheet"
href="style.css"
integrity="sha256-ghi…"
crossoriginstorage="https://a.example https://b.example"
/>
Con import attributes, para módulos:
const module = await import("resource.ext", {
with: { integrity: "sha256-abc…", crossOriginStorage: "*" },
});
Y con un modificador CSS para @font-face, que es donde más sentido tiene por el caso de las fuentes:
@font-face {
font-family: "Popular";
src: url("font.woff2" integrity("sha256-def…") cross-origin-storage(*));
}
Probarlo hoy
Estado real, sin adornos: COS es una propuesta en la WICG y no está implementada de forma nativa en ningún navegador. En Chrome Platform Status figura como Proposed, así que no hay flag que activar ni canal donde probarlo, tampoco en Canary. Lo que sí se puede hacer es probar el flujo completo con un polyfill, y eso ya dice bastante sobre si el modelo mental encaja con nuestro proyecto.
La vía práctica es la extensión Cross-Origin Storage, que inyecta navigator.crossOriginStorage en todas las páginas. No hace falta Canary ni activar ninguna flag: es una extensión normal, y existe para los tres motores.
| Navegador | Instalación |
|---|---|
| Chrome y derivados de Chromium | Chrome Web Store |
| Firefox, escritorio y Android | Firefox Add-ons |
| Safari en macOS, iOS y iPadOS | App Store |
El código de la extensión está bajo Apache 2.0. Por debajo usa CacheStorage en el service worker de la propia extensión, que vive fuera del particionado del navegador; por eso los ficheros sí se comparten de verdad entre orígenes distintos.
Conviene saber qué reproduce y qué no. La API imperativa, el modificador CSS y el atributo declarativo siguen la sintaxis del explainer tal cual. Las import attributes no: en lugar de with { crossOriginStorage: "*" } hay que pasar por type="module-cos" y navigator.crossOriginStorage.__non_standard__import().
El detalle que cambia lo que estamos midiendo
Aquí es fácil sacar la conclusión equivocada. El filtro de la Public Hash List viene desactivado por defecto en las opciones de la extensión. Con la PHL apagada, cualquier hash se revela como disponible al resto de orígenes, incluido un fichero de prueba nuestro que no ha visto nadie más.
Es decir: si validamos el flujo con la configuración de fábrica, estamos validando el happy path sin la defensa de privacidad que justifica toda la propuesta. El propio repositorio lo dice sin rodeos: requestFileHandle() permite a cualquier origen preguntar si un hash está cacheado, lo que convierte la presencia en caché en un oráculo de sondeo cross-site para ficheros poco comunes. La PHL existe justamente para cerrar eso.
Para probar el comportamiento realista hay que activarla en las opciones de la extensión. Y entonces la expectativa correcta se invierte: nuestro fichero de prueba debe dar NotFoundError desde el segundo origen. Ese fallo no es un error de montaje, es el gating funcionando.
Cómo montar la prueba
La forma más rápida, sin preparar nada, es usar las páginas de demo de la extensión, publicadas a propósito en dos orígenes distintos: web-ai-community.github.io y googlechrome.github.io. Como github.io está en la Public Suffix List, son sites distintos de verdad. Cargamos el ejemplo de Whisper en uno, dejamos que descargue el modelo, y lo abrimos en el otro: el segundo debería resolverlo desde COS.
Si preferimos controlar el fichero, dos puertos de localhost valen:
npx serve -l 5001 . # origin A
npx serve -l 5002 . # origin B
Con la extensión instalada, el flujo a validar es este:
- En
localhost:5001, descargar un fichero, calcular su SHA-256 concrypto.subtle.digesty guardarlo conorigins: '*'. - En
localhost:5002, pedir el mismo hash conrequestFileHandle()sincreate. - Mirar el panel Network del segundo origen. Si COS resuelve, el fichero aparece sin ninguna petición de red asociada.
Con la PHL activada, insisto, el paso 3 debe dar NotFoundError, porque un fichero nuestro nunca va a estar en esa lista. Para ver un hit real con el gating puesto hace falta un recurso que sí esté en la PHL, y ahí es donde sirven las páginas de demo de arriba.
Ese banco de pruebas está aquí montado, con detección de capacidades incluida. Si la etiqueta de arriba dice “COS no disponible”, es que la extensión no está instalada en este navegador y los pasos 2 y 3 quedan deshabilitados:
El hash que calcula es real, sale de crypto.subtle.digest('SHA-256', …) sobre los bytes descargados, y se puede contrastar en el terminal:
shasum -a 256 astropaper-og.jpg
# 07f07701f273c4717cf6f857eda793985a79cd99faeabb824278c854aa9e2990
Para probarlo en un proyecto real hay una segunda opción, vite-plugin-cross-origin-storage. Extrae las dependencias compartidas en chunks direccionados por contenido: si dos sitios compilan la misma dependencia en la misma versión, producen chunks byte a byte idénticos con el mismo SHA-256, que es justo lo que COS necesita para servirlos sin volver a la red.
// vite.config.ts
import { defineConfig } from "vite";
import { cosPlugin } from "vite-plugin-cross-origin-storage";
export default defineConfig({
plugins: [
cosPlugin({
packages: [/^(?:vue$|@vue\/)/],
}),
],
});
Dos detalles prácticos. El plugin solo actúa en build, así que hay que comprobarlo contra vite build && vite preview, no contra el servidor de desarrollo. Y para Nuxt existe nuxt-cos, un módulo que envuelve el plugin. Los dos avisan de lo mismo: son experimentales, el formato de chunk no es estable y no conviene depender de ellos en producción.
Una limitación honesta que documenta la propia extensión: para <script> clásicos, lo que acaba ejecutándose es lo que devuelve la red, así que el atributo siembra la caché pero no ahorra la descarga. Las fuentes vía CSS y los módulos son hoy los casos donde el ahorro se ve de verdad.
Conclusión
Volviendo al caso del principio: el iframe y el padre piden la misma imagen, y en Chrome se descarga dos veces porque el frame site forma parte de la clave. No es un error de configuración nuestro, no se soluciona con Cache-Control y no se soluciona con un CDN mejor.
Lo que sí está en nuestra mano:
- Dejar de asumir caché compartida entre sitios. Ese modelo mental lleva años caducado. Servir las librerías y las fuentes desde nuestro propio origen debería ser la decisión por defecto.
- Mirar el
Initiatoren el waterfall. Cuando un recurso aparece dos veces en la misma carga, casi siempre hay un iframe de por medio. - Contar los frames como fronteras de caché, no solo como fronteras de seguridad. En Chrome lo son.
- No confundir un MISS por partición con un MISS por eviction. Compartir partición es condición necesaria para el hit, no suficiente, y cada causa se soluciona en un sitio distinto.
Y para lo que no está en nuestra mano, Cross-Origin Storage es la propuesta que merece la pena seguir. Cambiar la clave de “de dónde viene” a “qué es” resuelve el problema sin deshacer el particionado, que es el único camino que no reabre lo que se cerró en 2020.
Referencias
- HTTP cache partitioning, Chrome for Developers
- Explainer de HTTP cache partitioning, con el análisis de clave doble frente a triple
- State Partitioning, MDN
- Say goodbye to resource caching across sites and domains, Stefan Judis
- Cross-Origin Storage (COS), explainer en la WICG
- Public Hash List, la lista que implementa el gating por k-anonimato
- Código de la extensión Cross-Origin Storage, el polyfill para Chrome, Firefox y Safari