Caché y freeze: render sin recalcular todo
El problema
Tienes un documento Quarto con un chunk que tarda 3 minutos en ejecutarse, un DESeq() sobre 30.000 genes, un modelo bayesiano, una llamada a una API lenta. Cada vez que cambias una palabra del texto, el render entero recalcula ese chunk. 30 cambios = 90 minutos esperando.
Quarto tiene dos mecanismos para esto:
cache: a nivel de chunk individual (heredado de knitr).freeze: a nivel de proyecto (más reciente y de uso global en websites y libros).
Los dos se pueden combinar. Aprender los dos paga 100× en productividad sobre proyectos grandes.
cache: true por chunk
El mecanismo clásico de knitr. Por chunk:
```{r}
#| label: modelo
#| cache: true
dds <- DESeq(dds) # toma minutos
res <- results(dds)
```
La primera vez: ejecuta el chunk y guarda el resultado en _cache/ (carpeta junto al .qmd). Renders sucesivos: si el código y dependencias no cambian, carga el resultado guardado. Casi instantáneo.
Cuándo se invalida la caché
knitr considera el chunk distinto (e invalida la caché) si:
- El código del chunk cambia.
- Las opciones del chunk cambian.
- Una dependencia declarada cambia (
dependson: ...).
knitr no detecta cambios en:
- Datos externos (un CSV que carga el chunk).
- Variables de chunks previos (a menos que las declares como dependencia).
- Funciones definidas en archivos
.Rfuente.
Esto es la causa #1 de “la caché está mintiendo”. Si tu chunk carga read.csv("datos.csv") y datos.csv cambia, el chunk no se recalcula automáticamente. Tienes que invalidar a mano.
Dependencias explícitas
```{r}
#| label: cargar-datos
#| cache: true
datos <- read.csv("datos.csv")
```
```{r}
#| label: modelo
#| cache: true
#| dependson: ["cargar-datos"]
modelo <- lm(y ~ x, data = datos)
```
dependson: ["cargar-datos"] le dice a knitr: “si cambia el chunk cargar-datos, invalida este también”.
Para invalidar también con cambios de un archivo externo:
```{r}
#| label: cargar-datos
#| cache: true
#| cache.extra: !expr file.info("datos.csv")$mtime
datos <- read.csv("datos.csv")
```
cache.extra con la fecha de modificación del fichero: si cambia, la caché se invalida. Patrón estándar para chunks que cargan datos.
Invalidar caché a mano
Cuando estás seguro de que la caché está stale:
quarto render documento.qmd --cache-refreshO bórrala manualmente:
rm -rf _cache/--cache-refresh regenera. rm la elimina (siguiente render la reconstruye).
Setup global de cache
Defaults para todo el documento:
---
execute:
cache: true
---Y excepciones en chunks individuales:
```{r}
#| cache: false # NO cachear este
resultado_rapido <- 1 + 1
```
Para chunks rápidos (cargar datos, sumar columnas), cachear es overhead innecesario. Para chunks lentos, el beneficio es enorme.
freeze: el sistema moderno para proyectos
freeze es el mecanismo de Quarto para proyectos enteros (websites, libros). Diferencia clave: cache opera por chunk, freeze opera por documento completo.
En _quarto.yml:
project:
type: website
execute:
freeze: autoTres modos:
auto: Quarto computa el documento la primera vez. En renders posteriores, no recomputa salvo que el.qmdhaya cambiado.true: Nunca recomputa, ni siquiera si cambias el.qmd. Tienes que invalidar a mano. Útil para CI con outputs ya generados localmente.false: Default. Siempre recomputa.
El output del compute se guarda en _freeze/. Esto se versiona en Git: cuando publicas el sitio en CI, el server no necesita ejecutar R/Python, sirve el output ya computado.
Cuándo cada uno
| Situación | Mecanismo |
|---|---|
| Documento solo, un chunk muy lento | cache: true en ese chunk |
| Documento solo, varios chunks moderados | execute: cache: true global |
| Website / book con muchas páginas | freeze: auto en _quarto.yml |
| Deploy a Netlify / GitHub Pages sin R en CI | freeze: auto + commit de _freeze/ |
Para proyectos serios, freeze: auto es casi obligatorio. Sin él, render de un libro de 30 capítulos = recalcular todos los chunks de los 30 capítulos cada vez.
_freeze/: qué versionar
_freeze/ contiene resultados pre-computados. Sí se versiona (entra en Git). Razones:
- Permite renderizar el sitio en CI sin R/Python instalado.
- Garantiza que todos los colaboradores ven el mismo output.
- Hace el deploy mucho más rápido (no se ejecuta nada en el servidor).
_cache/ (de knitr) suele estar en .gitignore, es output local, no se necesita compartir.
.gitignore típico de un proyecto Quarto:
/.quarto/
/_site/
/_cache/
*.rmarkdown
*_files/
Y _freeze/ no está en gitignore, se commitea.
El flujo idiomático con freeze
# Localmente: editas un .qmd
quarto preview # renderiza solo lo que cambió
# Antes de pushear:
git add _freeze/
git commit -m "actualiza analisis-regional"
git push
# En CI (Netlify, GitHub Actions, etc.):
quarto render # usa los _freeze/, no ejecuta R/Pythonquarto preview durante desarrollo recomputa solo el documento que editas. El resto sirve desde _freeze/.
Casos extremos: análisis muy pesados
Para análisis donde la ejecución toma horas (ML training, RNA-seq con miles de genes, simulaciones), patrón sugerido:
- Análisis en script
.Ro.pyseparado, que guarda resultados a.rds/.parquet. .qmdcarga los resultados ya computados.
# analisis.R (script separado)
modelo <- entrenar_random_forest(...)
saveRDS(modelo, "modelos/rf-2024.rds")```{r}
# en el .qmd, instantáneo
modelo <- readRDS("modelos/rf-2024.rds")
```
Esto separa el cómputo de la narrativa. El render del documento es siempre rápido. Cuando los datos cambian, ejecutas el script una vez y commits el .rds actualizado.
Es la solución más limpia para análisis serios, cache y freeze lo hacen automático, pero a veces tener el control explícito vale más.
Diferencias frente a Rmd
En R Markdown clásico, solo existía cache. Quarto añade freeze para resolver el caso de sitios y libros, donde knitr cache resultaba insuficiente.
Para proyectos pequeños (un solo .qmd), cache por chunk sigue siendo lo más cómodo. Para sitios y libros, freeze global.
Trampas habituales
- Caché stale silenciosa. Cambias un CSV, el chunk no se recalcula. Usa
cache.extraconfile.info()o invalida a mano. cache: trueen chunks rápidos. Overhead de E/S. Reserva para chunks de > 5 segundos.- Versionar
_cache/pero no_freeze/. Es al revés:_cache/es local,_freeze/se versiona para CI. freeze: trueaccidental. Significa “nunca recomputar”. Si tu documento parece “atascado” en una versión, comprueba el modo.- Cambio de versión de paquete + caché viva. Si actualizas DESeq2 y la caché es de antes, los resultados se cargan de la versión vieja. En cambios grandes de paquetes, borra
_cache/y_freeze/y regenera.
En la siguiente entrega
Has cerrado el bloque editorial. Lo que viene son sitios y libros Quarto: cómo se configura un _quarto.yml para website, los listings de blog, la navbar, y los detalles de configuración. Lo siguiente.