Kida's static analysis can determine which template blocks are safe to cache and at what scope. This article shows how to connect analysis results to runtime caching usingCachedBlocksDict— the same mechanism Bengal uses for 40-60% faster incremental builds.
The Pipeline
analyze → identify cacheable blocks → render once → cache → reuse
- Analyze templates with
block_metadata()ortemplate_metadata() - Identify site-scoped blocks (
cache_scope == "site") - Render those blocks once
- Cache the HTML output
- Inject cached HTML via
CachedBlocksDictfor subsequent renders
Step 1: Identify Cacheable Blocks
Use the analysis API to find blocks that can be cached:
from kida import Environment, FileSystemLoader
env = Environment(
loader=FileSystemLoader("templates/"),
preserve_ast=True, # Required for analysis
)
template = env.get_template("page.html")
# Get metadata for all blocks
meta = template.template_metadata()
if meta:
# Blocks safe to cache at the site level
site_blocks = meta.site_cacheable_blocks()
for block in site_blocks:
print(f"{block.name}: scope={block.cache_scope}, pure={block.is_pure}")
# nav: scope=site, pure=pure
# footer: scope=site, pure=pure
Cache Scope Meaning
| Scope | Meaning | Cache Strategy |
|---|---|---|
"site" |
Same output for every page | Cache once per build, reuse everywhere |
"page" |
Varies per page | Cache per page, invalidate on page change |
"none" |
Impure block | Do not cache |
"unknown" |
Cannot determine | Treat as uncacheable |
Step 2: Render and Cache Site Blocks
Render the first page to get site-scoped block HTML, then cache it:
# Render one page to populate site-scoped blocks
first_page_html = template.render(page=pages[0], site=site)
# Extract site-scoped block HTML by rendering blocks individually
site_cache = {}
for block in site_blocks:
# render_block renders a single block in isolation
block_html = template.render_block(block.name, page=pages[0], site=site)
site_cache[block.name] = block_html
Step 3: Inject Cached Blocks
UseCachedBlocksDictto intercept block lookups and return cached HTML:
from kida.template.cached_blocks import CachedBlocksDict
cached_names = frozenset(site_cache.keys())
stats = {"hits": 0, "misses": 0}
# Render remaining pages with cached site blocks
for page in pages[1:]:
# CachedBlocksDict wraps the normal blocks dict
cached_blocks = CachedBlocksDict(
original=None, # Template will populate via setdefault()
cached=site_cache, # Pre-rendered HTML
cached_names=cached_names,
stats=stats, # Optional hit/miss tracking
)
html = template.render(
page=page,
site=site,
_blocks=cached_blocks, # Inject cached blocks
)
CachedBlocksDict
CachedBlocksDictis a dict-like wrapper that intercepts block lookups:
from kida.template.cached_blocks import CachedBlocksDict
wrapper = CachedBlocksDict(
original=None, # Underlying blocks dict
cached={"nav": "<nav>...</nav>"}, # Pre-rendered HTML
cached_names=frozenset({"nav"}), # Which blocks are cached
stats={"hits": 0, "misses": 0}, # Optional statistics
)
When the template calls blocks.get("nav"), the wrapper returns a function that produces the cached HTML instead of re-rendering. For non-cached blocks, it falls through to the original dict.
Supported Operations
| Method | Behavior |
|---|---|
.get(key) |
Returns cached wrapper or falls through |
.setdefault(key, default) |
Cached blocks take precedence |
[key] |
Returns cached wrapper or falls through |
[key] = value |
Writes to original dict |
key in dict |
Checks both cached and original |
.keys() |
Union of cached and original keys |
.copy() |
Returns plain dict with cached wrappers |
Cache Statistics
Pass a shared stats dict to track hit/miss rates:
stats = {"hits": 0, "misses": 0}
for page in pages:
blocks = CachedBlocksDict(None, cache, cached_names, stats=stats)
template.render(page=page, site=site, _blocks=blocks)
hit_rate = stats["hits"] / (stats["hits"] + stats["misses"])
print(f"Block cache hit rate: {hit_rate:.1%}")
Complete Example
from kida import Environment, FileSystemLoader
from kida.template.cached_blocks import CachedBlocksDict
env = Environment(
loader=FileSystemLoader("templates/"),
preserve_ast=True,
)
template = env.get_template("page.html")
# 1. Analyze
meta = template.template_metadata()
site_blocks = meta.site_cacheable_blocks() if meta else []
site_block_names = frozenset(b.name for b in site_blocks)
# 2. Render first page (populates cache)
first_html = template.render(page=pages[0], site=site)
site_cache = {}
for block in site_blocks:
site_cache[block.name] = template.render_block(
block.name, page=pages[0], site=site
)
# 3. Render remaining pages with cache
stats = {"hits": 0, "misses": 0}
results = [first_html]
for page in pages[1:]:
blocks = CachedBlocksDict(None, site_cache, site_block_names, stats=stats)
results.append(template.render(page=page, site=site, _blocks=blocks))
print(f"Rendered {len(results)} pages")
print(f"Cache hits: {stats['hits']}, misses: {stats['misses']}")
When to Use Block Caching
| Scenario | Benefit |
|---|---|
| Static site generation | 40-60% faster full-site rebuilds |
| Server-side rendering with shared layout | Avoid re-rendering nav/footer per request |
| Multi-page PDF generation | Cache header/footer across pages |
| Email batch rendering | Cache shared template chrome |
Block caching is most effective when:
- Templates use inheritance with shared blocks (nav, footer, sidebar)
- The same template renders many times with different page data
- Site-wide data (menus, config) doesn't change between renders
See Also
- Static Analysis — Block-level dependency and purity analysis
- Profiling — Identify slow blocks worth caching
- Fragment Caching — Template-level
{% cache %}syntax - Performance — Overall optimization strategy