Block Caching

Connect static analysis to runtime block caching for framework-level optimization

4 min read 807 words

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
  1. Analyze templates withblock_metadata() or template_metadata()
  2. Identify site-scoped blocks (cache_scope == "site")
  3. Render those blocks once
  4. Cache the HTML output
  5. Inject cached HTML viaCachedBlocksDictfor 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