Caching incremental y el meta store
El store persiste entre builds y el meta store guarda la memoria —etags, fechas, tokens— que permite recargar solo lo que cambió. Peticiones condicionales, el digest para afinar por entrada, cuándo re-cargar y cómo todo se integra con defineCollection y el esquema de Zod en Astro 7.
La última pieza de la Content Layer es la que la vuelve rápida a escala: la capacidad de no rehacer el trabajo ya hecho. El store persiste entre builds, así que en la siguiente compilación las entradas del build anterior siguen ahí; y un segundo almacén, el meta store, guarda la memoria que un loader necesita para decidir qué recargar —un etag, una fecha, un token de sincronización—. Con esas dos memorias, un loader deja de traerlo todo cada vez y pasa a traer solo lo que de verdad cambió.
- Entender que el
storepersiste entre builds y por qué eso habilita el caché incremental. - Usar el
metastore para recordar entre compilaciones etags, fechas o tokens de sincronización. - Decidir cuándo una entrada debe recargarse y cuándo puede conservarse mediante su
digest. - Ver cómo loader,
store,metay el esquema dedefineCollectionse integran en un contenido tipado y cacheado.
El store persiste: la base del caché
Todo el caché incremental descansa sobre un hecho que ya asomó: el store no se tira al terminar el build, sino que Astro lo persiste en la carpeta .astro, en un fichero de datos. Cuando lanzas la siguiente compilación, el store no arranca vacío: contiene las entradas que dejó la anterior. Un loader que lo desee puede, por tanto, encontrarse con su propio trabajo previo y decidir qué conservar en lugar de rehacerlo.
Esa persistencia cambia la pregunta que un loader se hace. El loader inline se preguntaba “qué datos hay en la fuente” y los traía todos. Un loader incremental se pregunta “qué ha cambiado en la fuente desde la última vez” y trae solo eso. La diferencia es enorme cuando la fuente es grande y estable: si de diez mil entradas cambian tres, el segundo enfoque hace mil veces menos trabajo. El store persistente es lo que hace posible formular la segunda pregunta, porque conserva el “antes” contra el que comparar.
store persistente
Las entradas del build previo siguen en .astro. El punto de partida ya no es el vacio.
meta store
Memoria de texto entre builds: etags, fechas, tokens. El marcador que dice si algo cambio.
digest
La huella por entrada. Afina el ahorro: reescribe solo las que de verdad difieren.
store.clear
La renuncia al caché: olvida todo y empieza limpio cuando ya no confias en lo guardado.
La persistencia del store tiene un reverso: un caché puede quedar desactualizado o corrupto. Si un loader se comporta de forma extraña —muestra datos viejos que ya no deberían estar— borrar la carpeta .astro fuerza un arranque limpio, releyendo todo desde cero. Conviene ignorar .astro en el control de versiones —es un artefacto reconstruible— y saber que ese borrado es el “apaga y enciende” de la Content Layer.
El meta store: memoria entre builds
Para recargar solo lo que cambió, un loader necesita recordar algo del build anterior: normalmente, un marcador que la fuente entiende. Ese es el papel del meta store, un segundo almacén —separado del de las entradas— pensado para guardar pequeños valores de texto asociados a la colección. Su API es la de un mapa simple: meta.get(clave) lee, meta.set(clave, valor) escribe, meta.has comprueba y meta.delete borra. Solo guarda cadenas, porque su cometido no es contenido sino metadatos de sincronización.
load: async ({ store, meta, logger }) => {
const etag = meta.get('etag');
const res = await fetch(url, {
headers: etag ? { 'If-None-Match': etag } : {},
});
if (res.status === 304) {
logger.info('Sin cambios: conservo el store previo');
return;
}
const nuevoEtag = res.headers.get('etag');
if (nuevoEtag) meta.set('etag', nuevoEtag);
// ... procesar la respuesta y actualizar el store
}
El ejemplo condensa la idea entera. En el build anterior, el loader guardó el etag que la fuente le dio; en este, lo envía de vuelta en la cabecera If-None-Match. Si la fuente responde 304 Not Modified, nada cambió: el loader hace return sin tocar el store, y las entradas del build previo —que siguen ahí, por la persistencia— quedan como están. Solo cuando la fuente responde con datos nuevos el loader se pone a trabajar, y guarda el nuevo etag para la próxima vez.
Ese return temprano es la esencia del ahorro. No es que el loader procese rápido: es que no procesa en absoluto cuando no hay nada que hacer. El meta store es lo que le permite saberlo, porque le da una memoria que sobrevive al final del build. Sin él, cada compilación sería una amnesia total; con él, el loader recuerda dónde se quedó y retoma desde ahí.
A diferencia del store de entradas, el meta store almacena únicamente texto: su meta.set espera una cadena. Si necesitas guardar algo estructurado —un objeto con varios marcadores— serialízalo con JSON.stringify al escribir y recupéralo con JSON.parse al leer. Es una limitación deliberada: el meta es para metadatos pequeños de sincronización, no para contenido, y ese formato plano lo mantiene barato de persistir y de comparar entre builds.
Recargar solo lo que cambió
Cuando la fuente sí trae cambios, aún queda una segunda oportunidad de ahorro, más fina: actualizar solo las entradas que difieren, en vez de todas. Aquí entra el digest de la lección anterior. Si por cada entrada guardas su huella, al recibir los datos nuevos puedes calcular la huella de cada uno y compararla: las que coinciden no se tocan; las que cambian se reescriben. store.set colabora en esto, porque si le pasas un digest igual al que ya tenía la entrada, se salta la escritura y avisa de que nada cambió.
for (const item of items) {
const data = await parseData({ id: item.id, data: item });
const digest = generateDigest(data);
const cambio = store.set({ id: item.id, data, digest });
if (cambio) logger.info(`Actualizada la entrada ${item.id}`);
}
La granularidad del ahorro tiene, así, dos niveles. El grueso lo da el meta store: si la fuente entera no cambió, el loader ni siquiera entra a procesar. El fino lo da el digest: si la fuente cambió pero solo en unas pocas entradas, únicamente esas se reescriben. Un loader maduro combina los dos —primero pregunta si algo cambió, luego afina qué cambió— y así su coste se vuelve proporcional al cambio real, no al tamaño total del contenido.
Queda la pregunta de cuándo se dispara todo esto. En un build de producción, el loader corre una vez. En desarrollo, Astro observa y puede pedir recargas: si el loader trabaja con ficheros, el watcher del contexto le avisa de los cambios; y para fuentes que no son ficheros existe un mecanismo de refresco bajo demanda —refreshContextData— que permite forzar una recarga sin reiniciar el servidor, útil por ejemplo al recibir un webhook de un CMS que anuncia contenido nuevo.
Toda esta maquinaria se cierra sobre la pieza con la que empezó el nivel anterior: el esquema. parseData valida contra el schema que declaraste en defineCollection —o el que el propio loader expone— de modo que ni el caché ni las actualizaciones parciales pueden colar un dato mal formado. La integración es total: el loader trae y cachea, el store persiste, el meta recuerda, el digest afina y el esquema de Zod vigila que todo lo que entra al store, venga fresco o se conserve, cumpla el contrato.
flowchart TD
START[arranca el build] --> META[meta get etag]
META --> COND[fetch condicional con If None Match]
COND --> CH{la fuente cambio}
CH -->|304 no| KEEP[return y conservar el store previo]
CH -->|si| ITEMS[recorrer items]
ITEMS --> PD[parseData valida contra el schema]
PD --> DG[generateDigest y comparar]
DG --> SET[store set solo si cambio el digest]
SET --> SAVE[meta set nuevo etag]
style KEEP fill:#a6e3a1,color:#11111b
style SET fill:#a6e3a1,color:#11111b
style COND fill:#89b4fa,color:#11111bElegir el marcador de cambio
No todas las fuentes ofrecen el mismo marcador para saber si algo cambió, y elegir el adecuado es la decisión que define un loader incremental. El más fino es el ETag: una huella opaca que la fuente asocia a su estado y que solo cambia si el contenido cambia. El más común es la fecha de última modificación, que viaja en Last-Modified y se consulta con If-Modified-Since. Y el más potente, cuando la fuente lo ofrece, es un cursor o token de sincronización: un valor que representa “todo hasta aquí ya lo tienes” y que permite pedir solo lo posterior.
La diferencia entre ellos es la granularidad de la respuesta. Un ETag o una fecha te dicen si el conjunto entero cambió, pero no qué cambió: ante un cambio, aún tienes que traer y comparar. Un token de sincronización, en cambio, hace que la propia fuente te entregue solo el delta —las entradas nuevas, modificadas o borradas desde tu último token— y entonces el loader apenas necesita el digest para saber qué tocar. Guardar ese token en el meta store, en lugar de un simple etag, es lo que convierte una recarga incremental en una sincronización de verdad.
load: async ({ store, meta, logger }) => {
const cursor = meta.get('cursor');
const res = await fetch(`${url}?desde=${cursor ?? ''}`);
const { cambios, siguiente } = await res.json();
for (const c of cambios) {
if (c.borrado) store.delete(c.id);
else store.set({ id: c.id, data: c.data });
}
meta.set('cursor', siguiente);
logger.info(`Aplicados ${cambios.length} cambios`);
}
Ese patrón introduce la otra mitad del trabajo incremental, la que las peticiones condicionales simples olvidan: los borrados. Si una entrada desaparece de la fuente, el loader debe retirarla del store con store.delete, o quedará como un fantasma que sobrevive a su origen. Un delta bien hecho distingue las tres operaciones —alta, cambio y baja— y las aplica una a una; el store.clear seguido de repoblar las resolvía todas de golpe a costa de rehacerlo todo, pero el enfoque incremental las trata con cirugía.
Sea cual sea el marcador, la regla de oro es la misma: guárdalo solo después de haber procesado con éxito. Si escribes el nuevo marcador antes de terminar y la carga falla a mitad, el próximo build creerá que ya tiene lo que en realidad no llegó a guardar, y el fallo se volverá persistente e invisible. Actualizar el marcador es afirmar “este estado quedó a salvo en el store”, y esa afirmación debe ir al final, cuando de verdad es cierta.
El caché incremental parece una optimización técnica —guardar para no repetir— pero encierra una idea epistemológica sorprendentemente honda: toda caché es una apuesta sobre la estabilidad del mundo. Cuando un loader conserva el store previo porque la fuente devolvió un 304, está afirmando algo no trivial: que lo que era verdad en el build anterior sigue siéndolo ahora, salvo que la propia fuente diga lo contrario. Esa afirmación descansa por entero en la fiabilidad del marcador de cambio —el etag, la fecha, el digest—: si el marcador miente, si la fuente cambia sin actualizarlo, el caché sirve datos rancios con total convicción. Por eso los dos problemas clásicos de las cachés no son de rendimiento sino de verdad: la invalidación —saber cuándo lo guardado dejó de ser cierto— y la coherencia —garantizar que lo servido corresponde a la realidad—. La Content Layer no te libra de pensarlos; te da las herramientas —meta para recordar el marcador, digest para detectar la diferencia, store.clear para renunciar y empezar de cero— y deja en tus manos la política. Elegir bien el marcador de cambio es, en el fondo, elegir en qué confías: confías en que el etag de esa API es honesto, en que esa fecha de modificación se actualiza siempre, en que ese hash captura todo lo que te importa de una entrada. Cachear bien no es guardar agresivamente, sino saber con precisión bajo qué condición lo guardado sigue siendo válido, y tener el gesto de tirarlo en cuanto esa condición se rompe. La eficiencia es la recompensa; la corrección es la responsabilidad. Un loader que cachea sin una teoría clara de su propia invalidación no es rápido: es un mentiroso veloz.
- En un loader objeto, guarda con
meta.seteletago la fecha de última modificación que devuelva la fuente, y recupérala conmeta.geten la siguiente carga. - Haz una petición condicional —con
If-None-MatchoIf-Modified-Since— y, ante un304, hazreturnsin tocar elstore; comprueba que las entradas previas se conservan. - Guarda un
digestpor entrada y usa el valor que devuelvestore.setpara registrar con elloggersolo las entradas que de verdad cambiaron. - Borra la carpeta
.astroy observa cómo el primer build tras el borrado recarga todo; explica qué papel jugaba el caché que acabas de eliminar.