👤 Anonymous vs authenticated: cómo cambia la estrategia de caché en Drupal
Hasta ahora hemos hablado de caché como si todas las visitas fueran iguales.
Pero no lo son.
En Drupal existe una diferencia importante entre:
anonymous users
y:
authenticated users
Ambos pueden beneficiarse de caché.
Pero no de la misma manera.
Y entender esta diferencia es fundamental para evitar dos errores muy comunes:
desactivar demasiada caché porque hay usuarios autenticados
o:
intentar cachear como pública una respuesta que realmente depende de sesión o usuario
🧠 Empecemos por la idea más simple
Un usuario anónimo suele tener menos estado asociado.
Conceptualmente:
Anonymous visitor
→ no account-specific state
→ fewer variations
→ easier to reuse responses
Un usuario autenticado puede introducir:
permissions
roles
toolbar
user-specific content
messages
preferences
session data
Eso hace que el número de variantes potenciales aumente.
Pero no significa:
authenticated
=
uncacheable
Ese sería un error conceptual.
Drupal está diseñado precisamente para cachear también páginas dinámicas y personalizadas.
📄 Internal Page Cache está pensado principalmente para anónimos
Drupal Core incluye:
Internal Page Cache
o:
page_cache
Su objetivo principal es cachear respuestas completas para usuarios anónimos.
La razón es bastante intuitiva.
Si dos visitantes anónimos reciben exactamente:
same URL
same content
same representation
podemos reutilizar una respuesta completa.
Conceptualmente:
Anonymous A
GET /about
↓
generate page
↓
cache
Anonymous B
GET /about
↓
reuse full response
Esto puede ser extremadamente eficiente.
Drupal documenta Internal Page Cache precisamente como una solución orientada a tráfico anónimo. (drupal.org)
⚠️ Pero “anonymous” no siempre significa “stateless”
Aquí está la primera complicación.
Un usuario anónimo puede tener:
session
cart
cookie preferences
language preference
temporary state
Por ejemplo:
Anonymous A
Cart: 3 products
Anonymous B
Cart: empty
Ambos son anónimos desde el punto de vista de Drupal.
Pero sus respuestas no son iguales.
Eso significa que no podemos asumir:
anonymous
=
same response for everyone
La documentación de Drupal advierte expresamente sobre sitios que muestran contenido personalizado por sesión a usuarios anónimos, como shopping carts. En estos casos Internal Page Cache requiere especial atención. (drupal.org)
🧩 Aquí entra Dynamic Page Cache
Drupal también incluye:
Dynamic Page Cache
o:
dynamic_page_cache
Esta capa es especialmente importante porque no necesita pensar la página como un bloque indivisible.
Puede reutilizar gran parte del render tree aunque una pequeña sección sea dinámica.
Imaginemos:
Page
├── Header
├── Menu
├── Main content
├── Sidebar
└── User block
Quizá:
Header
Menu
Main content
Sidebar
pueden reutilizarse.
Y solo:
User block
depende del usuario actual.
Dynamic Page Cache permite aprovechar esa diferencia.
🧠 Authenticated no significa recalcular todo
Supongamos un usuario autenticado que visita:
/news/article-42
La página podría contener:
Article title
Article body
Image
Related articles
Main navigation
Footer
User toolbar
Personal menu
Sería muy costoso asumir:
because toolbar is personal
→ render entire page again
En realidad Drupal puede mantener cacheable una gran parte.
Conceptualmente:
Article
→ shared
Navigation
→ maybe shared by permissions
Footer
→ shared
Toolbar
→ user/permissions dependent
Eso es mucho más eficiente.
🌍 Cache contexts son fundamentales para usuarios autenticados
En el artículo anterior vimos que los cache contexts representan:
en qué situación cambia el output
Con usuarios autenticados aparecen contextos especialmente importantes.
Por ejemplo:
user
user.roles
user.permissions
No significan lo mismo.
👤 user
El contexto:
user
significa que el resultado puede variar por usuario individual.
Conceptualmente:
User 42
→ cached variant A
User 99
→ cached variant B
User 120
→ cached variant C
Esto puede ser correcto si mostramos algo realmente personal.
Por ejemplo:
Hello, Daniel
o:
Your saved items
Pero tiene alta cardinalidad.
En un sitio con miles de usuarios puede generar muchísimas variantes.
🎭 user.roles
Ahora supongamos que un bloque cambia solamente según el rol.
Por ejemplo:
editor
administrator
customer
Podría ser suficiente usar:
user.roles
Conceptualmente:
Editors
→ one variant
Administrators
→ another variant
Customers
→ another variant
Eso puede ser mucho más eficiente que una variante por usuario.
🔐 user.permissions
A veces el resultado depende realmente de permisos.
Por ejemplo:
Can edit content?
Can access administration pages?
Can view unpublished content?
Entonces:
user.permissions
puede expresar mejor la dependencia.
Drupal utiliza cache contexts jerárquicos y optimizables para evitar variaciones innecesarias. (drupal.org)
La idea práctica es:
usar el contexto menos específico que siga siendo correcto
📈 Cardinalidad importa
Podemos visualizarlo así:
language
→ 2–5 variants
role
→ maybe 5–20 variants
permissions
→ potentially more
user
→ thousands or millions
Cuantas más variantes generamos, menos reutilización conseguimos.
Por eso una buena cacheability metadata no solo debe ser correcta.
También debe ser razonablemente específica.
🔐 Las sesiones cambian el panorama
Una sesión puede introducir estado.
Por ejemplo:
logged-in user
shopping cart
temporary form state
messages
wizard progress
Si una respuesta depende de la sesión, tenemos que tratarla con mucho más cuidado.
Pero nuevamente:
la existencia de sesión no significa que toda la página tenga que ser dinámica
La clave es aislar la parte que depende de esa sesión.
💬 Drupal messages
Un ejemplo clásico son los mensajes:
Your content has been saved.
Ese mensaje pertenece a un request o usuario concreto.
No queremos que:
User A
sees "Your content has been saved"
y después:
User B
receives same cached message
Drupal debe mantener ese contenido separado.
Este tipo de elementos ayuda a entender por qué una página autenticada tiene necesidades distintas a una página pública simple.
🛠️ Toolbar
La Toolbar de Drupal es otro buen ejemplo.
Un administrador puede ver:
Manage
Shortcuts
Administration
mientras otro usuario autenticado puede no ver nada de eso.
La página principal puede ser la misma.
Pero ese pequeño componente cambia según permisos.
La estrategia ideal es:
shared page content
+
permission-dependent component
no:
entire page uncacheable
🧩 Render cache sigue siendo útil
Incluso cuando la página completa no puede reutilizarse como una sola respuesta, Render Cache puede seguir ayudando muchísimo.
Por ejemplo:
Node 42
puede tener un render cache reutilizable.
Una View puede tener su propio resultado cacheable.
Un menú también.
Un bloque también.
Conceptualmente:
Authenticated request
↓
Page assembled
↓
many components already cached
↓
only some pieces recomputed
Por eso debemos evitar pensar únicamente en:
full page HIT
como medida de éxito.
🧠 Una página autenticada puede ser rápida sin Page Cache
Supongamos:
X-Drupal-Cache
→ not applicable
pero:
Dynamic Page Cache
→ useful
Render Cache
→ useful
Entity render cache
→ useful
Views cache
→ useful
La experiencia puede seguir siendo excelente.
La pregunta correcta no es:
“¿Por qué no veo full-page cache?”
sino:
“¿Cuánto trabajo está reutilizando Drupal dentro de este request?”
💤 Lazy builders son especialmente útiles aquí
Drupal permite usar:
#lazy_builder
para componentes dinámicos.
Imaginemos:
Page
├── Article
├── Related content
├── Footer
└── User notifications
Podemos mantener:
Article
Related content
Footer
cacheados.
Y resolver:
User notifications
de forma lazy.
Conceptualmente:
cached shell
+
dynamic fragment
Eso ayuda muchísimo cuando existe personalización limitada.
🚀 BigPipe
Drupal Core incluye BigPipe.
La idea es enviar primero lo que ya podemos renderizar rápidamente:
Page shell
y completar después placeholders dinámicos.
Conceptualmente:
Request
↓
Send cached/static parts
↓
Browser starts rendering
↓
Resolve dynamic placeholders
↓
Inject them
Esto es especialmente útil en páginas autenticadas.
Porque aunque ciertos componentes dependan de usuario o sesión, el visitante no tiene por qué esperar a que toda la página termine de calcularse antes de ver algo.
🧠 BigPipe no sustituye una buena cacheability
Esto merece subrayarse.
BigPipe funciona bien cuando Drupal entiende:
what is cacheable
what is dynamic
Si marcamos toda la página:
max-age: 0
sin necesidad, estamos dificultando ese trabajo.
BigPipe no es un reemplazo de:
tags
contexts
max-age
Es una herramienta que se beneficia de que esos conceptos estén bien definidos.
🛒 Anonymous + cart: un caso especialmente interesante
Drupal Commerce nos obliga a pensar más cuidadosamente.
Supongamos un visitante anónimo:
User A
cart = 2 items
y otro:
User B
cart = empty
Una homepage podría contener:
Header
Hero
Products
Mini-cart
Footer
Quizá solamente:
Mini-cart
depende del carrito.
Sería muy costoso convertir:
Hero
Products
Footer
en contenido completamente dinámico solamente por ese bloque.
La estrategia ideal suele parecerse a:
Shared/cached page
+
dynamic cart fragment
Este caso será central cuando entremos específicamente en Drupal Commerce.
🍪 Cookies y personalización
También debemos distinguir entre diferentes tipos de cookies.
Por ejemplo:
analytics cookie
consent cookie
session cookie
cart-related cookie
No todas tienen las mismas consecuencias.
Y una cookie existente no significa automáticamente:
everything private
Pero sí debemos entender:
¿la respuesta realmente cambia según esa cookie?
Si cambia, necesitamos representar esa dependencia correctamente o aislarla.
🌐 HTTP caching y usuarios autenticados
Aquí aparece otro nivel.
Un browser puede cachear contenido privado para su propio usuario.
Pero una caché compartida, como un CDN, tiene requisitos mucho más estrictos.
No queremos que una respuesta personalizada para:
User A
termine siendo servida a:
User B
Por eso headers como:
Cache-Control: private
son completamente legítimos en determinadas respuestas personalizadas.
La optimización no consiste en eliminar private indiscriminadamente.
Consiste en decidir:
What is truly private?
What can be shared?
🔐 Public vs private
Podemos pensar:
public
→ shared caches may reuse it
private
→ intended for a single user's cache
Una página completamente pública puede ser candidata para:
CDN
Varnish
shared reverse proxy
Una página personalizada puede requerir:
private
Pero quizá algunos de sus recursos o fragments siguen siendo compartibles internamente por Drupal.
Ahí está la diferencia entre:
HTTP response caching
y:
Drupal internal caching
🧪 Cómo comparar anonymous y authenticated
Una técnica sencilla es analizar la misma URL de dos maneras.
Por ejemplo:
/home
Primero:
curl -sI https://example.com/
como visitante anónimo.
Después revisar la misma ruta dentro de una sesión autenticada usando DevTools.
Comparar:
Cache-Control
X-Drupal-Dynamic-Cache
Set-Cookie
Vary
y, en desarrollo:
X-Drupal-Cache-Contexts
Esto puede revelar inmediatamente qué cambia.
🔎 Preguntas útiles
Cuando una página autenticada parece lenta:
¿Qué parte depende realmente del usuario?
Whole page?
Toolbar?
Notification?
Saved state?
¿Estamos usando user cuando bastaría user.permissions?
¿Hay un componente con max-age: 0?
¿Puede aislarse mediante lazy builder?
¿Dynamic Page Cache está activo?
¿Estamos confundiendo “no full-page cache” con “no caching”?
Estas preguntas suelen ser mucho más productivas que intentar forzar un HIT.
🚫 No desactivar caché globalmente para usuarios autenticados
Una solución tentadora puede ser:
authenticated users
→ no cache
Eso desperdicia una enorme cantidad de trabajo.
Incluso una página altamente personalizada puede contener componentes como:
site logo
navigation structure
content entities
media
configuration
translations
que pueden reutilizarse.
Drupal está diseñado para manejar precisamente este tipo de composición.
🔴 Otro anti-pattern: max-age: 0 en bloques personalizados
Supongamos que hacemos:
public function build() {
return [
'#markup' => $this->currentUser()->getDisplayName(),
'#cache' => [
'max-age' => 0,
],
];
}
Funciona.
Pero quizá estamos diciendo demasiado.
Podríamos expresar:
varies by user
con un cache context:
'#cache' => [
'contexts' => [
'user',
],
]
y mantener el resultado cacheable por usuario.
La diferencia es enorme.
🧠 Dynamic no significa uncacheable
Esta es probablemente la frase clave del artículo:
Contenido dinámico no es necesariamente contenido no cacheable.
Algo puede variar.
Y seguir siendo cacheable.
Por ejemplo:
Language-dependent
→ variants by language
Permission-dependent
→ variants by permissions
User-dependent
→ variants by user
La caché no requiere que exista una única respuesta universal.
Requiere que Drupal sepa cuándo dos requests son equivalentes.
🧩 Cache contexts responden exactamente esa pregunta
Podemos pensar:
Request A
and
Request B
pueden reutilizar el mismo resultado si sus contextos relevantes coinciden.
Por ejemplo:
same language
same permissions
same route
Drupal puede reutilizar una variante.
Si cambia alguno:
different language
selecciona otra.
Ese es el verdadero poder de los cache contexts.
⚖️ Seguridad y caché están relacionadas
Una metadata de caché incorrecta no es solamente un problema de rendimiento.
También puede convertirse en un problema funcional o incluso de seguridad.
Supongamos que algo personalizado no declara:
user
como contexto.
Drupal podría reutilizar una respuesta entre usuarios cuando no debería.
Por eso:
Cacheability correcta significa tanto rendimiento como aislamiento correcto del contenido.
Nunca debemos eliminar contexts solo para aumentar los HITs.
🔐 Más caché no siempre es mejor
Si la información debe ser privada:
keep it private
Si depende de usuario:
vary by user
Si no puede reutilizarse:
don't cache it
La optimización correcta es:
máxima reutilización compatible con la corrección y la seguridad
No:
“cachear todo.”
📊 Medir ambos escenarios
Cuando hacemos pruebas de rendimiento conviene medir:
Anonymous
Authenticated
por separado.
Por ejemplo:
Homepage anonymous
TTFB: 70 ms
Homepage authenticated
TTFB: 180 ms
Eso no significa necesariamente que exista un problema.
El usuario autenticado puede requerir más trabajo.
La pregunta es:
Can we explain that difference?
Y:
Can we reduce unnecessary dynamic work?
🧭 Nuestro modelo mental
Podemos resumirlo así:
Anonymous
↓
Can often use full page cache
↓
Dynamic Page Cache still useful
mientras:
Authenticated
↓
Full response usually more contextual
↓
Dynamic Page Cache
Render Cache
Lazy Builders
BigPipe
become especially important
Y para ambos:
correct cacheability metadata
=
foundation
✅ Checklist para usuarios autenticados
Cuando revisemos una página autenticada:
[ ] ¿Dynamic Page Cache está activo?
[ ] ¿Qué cache contexts tiene?
[ ] ¿Aparece `user`?
[ ] ¿Realmente necesita `user`?
[ ] ¿Bastaría `user.roles` o `user.permissions`?
[ ] ¿Existe `max-age: 0`?
[ ] ¿Qué componente lo introduce?
[ ] ¿Hay sesión?
[ ] ¿Qué depende de esa sesión?
[ ] ¿Puede aislarse con lazy builder?
[ ] ¿BigPipe está ayudando?
[ ] ¿Estamos midiendo TTFB?
✅ Checklist para usuarios anónimos
Y para anonymous:
[ ] ¿Internal Page Cache está activo?
[ ] ¿Dynamic Page Cache está activo?
[ ] ¿La página es realmente igual para todos?
[ ] ¿Existe sesión anónima?
[ ] ¿Hay carrito?
[ ] ¿Hay personalización por cookie?
[ ] ¿Qué devuelve Cache-Control?
[ ] ¿MISS se convierte en HIT?
[ ] ¿Estamos creando cookies innecesarias?
🧠 La idea principal
Si debemos quedarnos con una sola idea:
Drupal no divide el mundo entre “páginas estáticas cacheables” y “usuarios autenticados no cacheables”.
Tiene un modelo mucho más granular.
Puede decir:
this component
depends on language
this one
depends on permissions
this one
depends on user
this one
depends on cart
this one
is shared by everyone
y ensamblar la respuesta correctamente.
Por eso Dynamic Page Cache, Render Cache, cache contexts, lazy builders y BigPipe son tan importantes.
La pregunta correcta no es:
“¿Esta página es dinámica?”
sino:
“¿Qué parte es dinámica, para quién cambia y qué podemos seguir reutilizando?”
🔜 En el siguiente artículo
Hay un caso donde todas estas ideas se vuelven especialmente interesantes:
Drupal Commerce
Un carrito depende de sesión.
Los precios pueden variar.
Las promociones pueden cambiar.
El mini-cart aparece en páginas que, por lo demás, son completamente públicas.
Entonces aparecen preguntas como:
- ¿El carrito debería ser cacheable?
- ¿Por qué un mini-cart puede afectar a toda la página?
- ¿Cómo evitar que Commerce convierta páginas públicas en respuestas innecesariamente dinámicas?
- ¿Qué papel tienen lazy builders y placeholders?
👉 En el siguiente artículo veremos 🛒 “Drupal Commerce y caché: cómo mantener rápido un sitio con carrito, precios y sesión”.