drupal-dynamic-cache
SKILL.md
Dynamic Page Cache & BigPipe
These modules cache pages for authenticated users while handling personalized content via placeholders.
When to Use
- Improving performance for logged-in users
- Debugging UNCACHEABLE responses
- Implementing personalized content that doesn't break caching
- Understanding BigPipe streaming behavior
How Dynamic Page Cache Works
Request → Check cache
├── HIT: Serve cached structure → Replace placeholders → Response
└── MISS: Render page → Auto-placeholder dynamic parts → Cache → Response
Response Headers
| Header | Values | Meaning |
|---|---|---|
X-Drupal-Dynamic-Cache |
HIT |
Served from cache |
MISS |
Rendered and cached | |
UNCACHEABLE |
Cannot cache (max-age:0) |
Auto-Placeholder Conditions
Content is automatically placeholdered when:
max-age: 0- Cache context
userorsession #create_placeholderis TRUE
Examples
Example 1: Check Dynamic Cache Status
Input: "How do I know if Dynamic Page Cache is working?"
Output:
# Check response header
curl -sI -H "Cookie: SESS123=abc" https://site.com/ | grep X-Drupal-Dynamic-Cache
# Expected results:
# X-Drupal-Dynamic-Cache: HIT → Working, served from cache
# X-Drupal-Dynamic-Cache: MISS → First request, now cached
# X-Drupal-Dynamic-Cache: UNCACHEABLE → Problem! Something has max-age:0
Example 2: Debug UNCACHEABLE Response
Input: "My page shows UNCACHEABLE, how do I fix it?"
Output:
// Enable debug headers to see what's wrong
// settings.local.php
$settings['http.response.debug_cacheability_headers'] = TRUE;
// Check headers:
// X-Drupal-Cache-Max-Age: 0 → Something set max-age:0
// Common culprits:
// 1. Block with max-age:0
// 2. Form without lazy builder
// 3. Custom module not setting cache metadata
// Find the source with Xdebug breakpoint:
// File: Cache.php, Method: mergeMaxAges(), Condition: $a === 0 || $b === 0
Example 3: Lazy Builder for User Content
Input: "I need to show user-specific data without breaking cache"
Output:
// DON'T do this - breaks page caching:
$build['greeting'] = [
'#markup' => 'Hello ' . $user->getDisplayName(),
'#cache' => ['contexts' => ['user']], // Page becomes per-user!
];
// DO this - use lazy builder:
$build['greeting'] = [
'#lazy_builder' => ['my_module.lazy:userGreeting', [$user->id()]],
'#create_placeholder' => TRUE,
];
// LazyBuilders service
class LazyBuilders implements TrustedCallbackInterface {
public static function trustedCallbacks() {
return ['userGreeting'];
}
public function userGreeting(int $uid): array {
$user = User::load($uid);
return [
'#markup' => 'Hello ' . $user->getDisplayName(),
'#cache' => [
'contexts' => ['user'],
'tags' => ['user:' . $uid],
],
];
}
}
Example 4: BigPipe Streaming
Input: "How does BigPipe work with placeholders?"
Output:
<!-- Initial HTML sent immediately: -->
<div data-big-pipe-placeholder-id="callback=my_module.lazy%3AuserGreeting&args[0]=5">
<span class="placeholder">Loading...</span>
</div>
<!-- Later, streamed via script tag: -->
<script type="application/vnd.drupal-ajax">
[{"command":"insert",
"selector":"[data-big-pipe-placeholder-id='callback=my_module.lazy%3AuserGreeting&args[0]=5']",
"data":"Hello John!"}]
</script>
<!-- JavaScript replaces placeholder content automatically -->
Example 5: Force Placeholder Creation
Input: "I want to placeholder content even without user context"
Output:
$build['dynamic'] = [
'#lazy_builder' => ['my_module.lazy:dynamicContent', []],
'#create_placeholder' => TRUE, // Force placeholder regardless of contexts
];
Example 6: Configure Auto-Placeholder Conditions
Input: "How do I customize what gets auto-placeholdered?"
Output:
# services.yml
parameters:
renderer.config:
auto_placeholder_conditions:
max-age: 0
contexts:
- 'session'
- 'user'
- 'my_custom_context' # Add custom context
tags: [] # Tags that trigger placeholder (usually empty)
Example 7: BigPipe Requirements
Input: "BigPipe isn't working, what could be wrong?"
Output:
# Check server configuration:
# Nginx - disable buffering (auto-handled via X-Accel-Buffering header)
location ~ \.php$ {
# BigPipe sends: X-Accel-Buffering: no
# Nginx respects this automatically
}
# Apache - works by default
# Common issues:
# 1. Output buffering in PHP - check ob_start() calls
# 2. Reverse proxy buffering - configure Varnish/CDN
# 3. JavaScript disabled - falls back to non-streaming
# Verify BigPipe is enabled
drush pm:list | grep big_pipe
Example 8: Anonymous vs Authenticated Caching
Input: "Explain the caching difference between anonymous and logged-in users"
Output:
Anonymous User:
┌─────────────────────────────────────────┐
│ Page Cache → HIT → Full page served │
│ (Dynamic Page Cache skipped) │
│ No placeholders, no BigPipe │
└─────────────────────────────────────────┘
Authenticated User:
┌─────────────────────────────────────────┐
│ Page Cache → SKIP (has session cookie) │
│ Dynamic Page Cache → HIT/MISS │
│ Placeholders replaced via BigPipe │
└─────────────────────────────────────────┘
// Check with curl:
// Anonymous
curl -sI https://site.com/ | grep X-Drupal
// X-Drupal-Cache: HIT
// Authenticated (with session cookie)
curl -sI -H "Cookie: SESSabc=xyz" https://site.com/ | grep X-Drupal
// X-Drupal-Dynamic-Cache: HIT
Common Mistakes
| Mistake | Impact | Solution |
|---|---|---|
| max-age:0 without lazy builder | Page UNCACHEABLE | Use #lazy_builder |
user context on blocks |
Per-user cache entries | Use user.roles or lazy builder |
| Disabling Dynamic Page Cache | Slow authenticated pages | Fix underlying max-age issues |
| Object args to lazy builder | Runtime error | Use scalar values only |
Debugging Checklist
# 1. Check Dynamic Cache status
curl -sI -H "Cookie: SESS=x" https://site.com/ | grep X-Drupal-Dynamic-Cache
# 2. Enable debug headers
# settings.local.php: $settings['http.response.debug_cacheability_headers'] = TRUE;
# 3. Check max-age
curl -sI https://site.com/ | grep X-Drupal-Cache-Max-Age
# 4. Verify BigPipe module
drush pm:list | grep big_pipe
Weekly Installs
8
Repository
sparkfabrik/sf-…-copilotGitHub Stars
2
First Seen
Jan 26, 2026
Security Audits
Installed on
cursor8
antigravity8
claude-code8
codex8
mcpjam7
kiro-cli7