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