Caching

Template Fragment Caching

Template fragment caching allows you to cache specific portions of templates rather than entire pages, providing fine-grained control over what gets cached and when. This approach is particularly effective for templates with mixed dynamic and static content, enabling you to cache expensive template fragments while keeping other parts dynamic and personalized.

Template Fragment Caching

Template fragment caching allows you to cache specific portions of templates rather than entire pages, providing fine-grained control over what gets cached and when. This approach is particularly effective for templates with mixed dynamic and static content, enabling you to cache expensive template fragments while keeping other parts dynamic and personalized.

Basic Template Fragment Caching

Using the cache Template Tag

<!-- Basic fragment caching -->
{% load cache %}

<!-- Cache a simple fragment for 5 minutes -->
{% cache 300 sidebar %}
    <div class="sidebar">
        <h3>Popular Posts</h3>
        <ul>
            {% for post in popular_posts %}
                <li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
            {% endfor %}
        </ul>
    </div>
{% endcache %}

<!-- Cache with variables in the key -->
{% cache 600 user_profile user.id %}
    <div class="user-profile">
        <h2>{{ user.get_full_name }}</h2>
        <p>Member since: {{ user.date_joined|date:"F Y" }}</p>
        <p>Posts: {{ user.posts.count }}</p>
    </div>
{% endcache %}

<!-- Cache with multiple variables -->
{% cache 900 post_comments post.id page_number %}
    <div class="comments">
        <h3>Comments (Page {{ page_number }})</h3>
        {% for comment in comments %}
            <div class="comment">
                <strong>{{ comment.author.username }}</strong>
                <span class="date">{{ comment.created_at|timesince }} ago</span>
                <p>{{ comment.content|linebreaks }}</p>
            </div>
        {% endfor %}
    </div>
{% endcache %}

Advanced Fragment Caching

<!-- Advanced template fragment caching -->
{% load cache %}

<!-- Cache with conditional logic -->
{% if user.is_authenticated %}
    {% cache 300 user_dashboard user.id user.last_login %}
        <div class="dashboard">
            <h2>Welcome back, {{ user.first_name }}!</h2>
            <div class="stats">
                <span>Posts: {{ user.posts.count }}</span>
                <span>Comments: {{ user.comments.count }}</span>
            </div>
        </div>
    {% endcache %}
{% else %}
    {% cache 1800 anonymous_welcome %}
        <div class="welcome">
            <h2>Welcome to Our Blog</h2>
            <p>Please <a href="{% url 'login' %}">login</a> to access your dashboard.</p>
        </div>
    {% endcache %}
{% endif %}

<!-- Cache with language support -->
{% get_current_language as LANGUAGE_CODE %}
{% cache 3600 navigation LANGUAGE_CODE %}
    <nav class="main-navigation">
        <ul>
            <li><a href="{% url 'home' %}">{% trans "Home" %}</a></li>
            <li><a href="{% url 'blog' %}">{% trans "Blog" %}</a></li>
            <li><a href="{% url 'about' %}">{% trans "About" %}</a></li>
            <li><a href="{% url 'contact' %}">{% trans "Contact" %}</a></li>
        </ul>
    </nav>
{% endcache %}

<!-- Cache with request-specific data -->
{% cache 600 category_posts category.slug request.GET.page %}
    <div class="category-posts">
        <h2>{{ category.name }}</h2>
        {% for post in posts %}
            <article class="post-summary">
                <h3><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
                <p>{{ post.excerpt }}</p>
                <span class="meta">{{ post.created_at|date:"M d, Y" }}</span>
            </article>
        {% endfor %}
    </div>
{% endcache %}

Dynamic Cache Keys

Custom Template Tags for Cache Keys

# templatetags/cache_tags.py
from django import template
from django.core.cache.utils import make_template_fragment_key
from django.core.cache import cache
import hashlib

register = template.Library()

@register.simple_tag
def cache_key(*args):
    """Generate a cache key from arguments."""
    key_parts = [str(arg) for arg in args]
    return ':'.join(key_parts)

@register.simple_tag
def hash_cache_key(*args):
    """Generate a hashed cache key for long keys."""
    key_parts = [str(arg) for arg in args]
    full_key = ':'.join(key_parts)
    
    if len(full_key) > 200:  # Memcached key length limit
        return hashlib.md5(full_key.encode()).hexdigest()
    
    return full_key

@register.simple_tag
def versioned_cache_key(base_key, version_key):
    """Generate a versioned cache key."""
    version = cache.get(f"version:{version_key}", 1)
    return f"{base_key}:v{version}"

@register.simple_tag
def user_cache_key(user, *args):
    """Generate user-specific cache key."""
    if user.is_authenticated:
        key_parts = [f"user_{user.id}"] + [str(arg) for arg in args]
    else:
        key_parts = ["anonymous"] + [str(arg) for arg in args]
    
    return ':'.join(key_parts)

@register.simple_tag(takes_context=True)
def request_cache_key(context, base_key, *args):
    """Generate cache key including request parameters."""
    request = context['request']
    
    key_parts = [base_key]
    key_parts.extend([str(arg) for arg in args])
    
    # Add relevant request parameters
    if request.GET:
        sorted_params = sorted(request.GET.items())
        param_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
        key_parts.append(hashlib.md5(param_str.encode()).hexdigest())
    
    return ':'.join(key_parts)

@register.simple_tag
def conditional_cache_key(condition, true_key, false_key):
    """Generate cache key based on condition."""
    return true_key if condition else false_key
<!-- Using custom cache key tags -->
{% load cache_tags cache %}

<!-- Hashed cache key for complex data -->
{% hash_cache_key "post_list" category.slug tag.slug sort_order as cache_key %}
{% cache 600 cache_key %}
    <!-- Complex post listing -->
{% endcache %}

<!-- Versioned cache key -->
{% versioned_cache_key "sidebar" "content_version" as versioned_key %}
{% cache 1800 versioned_key %}
    <!-- Sidebar content that can be bulk invalidated -->
{% endcache %}

<!-- User-specific cache key -->
{% user_cache_key user "preferences" as user_key %}
{% cache 900 user_key %}
    <div class="user-preferences">
        <!-- User-specific content -->
    </div>
{% endcache %}

<!-- Request-aware cache key -->
{% request_cache_key "search_results" query as search_key %}
{% cache 300 search_key %}
    <div class="search-results">
        <!-- Search results that vary by query parameters -->
    </div>
{% endcache %}

Nested Fragment Caching

Hierarchical Caching Strategy

<!-- Nested fragment caching -->
{% load cache %}

<!-- Outer cache: entire post list -->
{% cache 1800 post_list category.slug page_number %}
    <div class="post-list">
        <h2>{{ category.name }} Posts</h2>
        
        {% for post in posts %}
            <!-- Inner cache: individual post summary -->
            {% cache 3600 post_summary post.id post.updated_at %}
                <article class="post-summary">
                    <h3><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
                    
                    <!-- Even more granular: post metadata -->
                    {% cache 7200 post_meta post.id %}
                        <div class="post-meta">
                            <span>By {{ post.author.get_full_name }}</span>
                            <span>{{ post.created_at|date:"M d, Y" }}</span>
                            <span>{{ post.comments.count }} comments</span>
                        </div>
                    {% endcache %}
                    
                    <p>{{ post.excerpt }}</p>
                    
                    <!-- Cache tags separately -->
                    {% cache 3600 post_tags post.id %}
                        <div class="tags">
                            {% for tag in post.tags.all %}
                                <span class="tag">{{ tag.name }}</span>
                            {% endfor %}
                        </div>
                    {% endcache %}
                </article>
            {% endcache %}
        {% endfor %}
        
        <!-- Cache pagination separately -->
        {% cache 600 pagination category.slug page_number total_pages %}
            <div class="pagination">
                {% if page_obj.has_previous %}
                    <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
                {% endif %}
                
                <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
                
                {% if page_obj.has_next %}
                    <a href="?page={{ page_obj.next_page_number }}">Next</a>
                {% endif %}
            </div>
        {% endcache %}
    </div>
{% endcache %}

Smart Cache Invalidation for Nested Fragments

# utils/fragment_cache.py
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key

class FragmentCacheManager:
    """Manage template fragment cache invalidation."""
    
    def __init__(self):
        self.cache = cache
    
    def invalidate_fragment(self, fragment_name, *args):
        """Invalidate a specific template fragment."""
        cache_key = make_template_fragment_key(fragment_name, args)
        self.cache.delete(cache_key)
    
    def invalidate_post_fragments(self, post):
        """Invalidate all fragments related to a post."""
        fragments_to_invalidate = [
            ('post_summary', [post.id, post.updated_at]),
            ('post_meta', [post.id]),
            ('post_tags', [post.id]),
            ('post_comments', [post.id]),
        ]
        
        for fragment_name, args in fragments_to_invalidate:
            self.invalidate_fragment(fragment_name, *args)
        
        # Also invalidate list views that might contain this post
        self.invalidate_post_list_fragments(post)
    
    def invalidate_post_list_fragments(self, post):
        """Invalidate post list fragments that might contain this post."""
        # This is a simplified approach - in practice, you might want
        # to track which lists contain which posts
        
        # Invalidate category lists
        if post.category:
            for page in range(1, 6):  # Assume max 5 pages
                self.invalidate_fragment('post_list', post.category.slug, page)
        
        # Invalidate author lists
        for page in range(1, 4):  # Assume max 3 pages for author
            self.invalidate_fragment('author_posts', post.author.id, page)
        
        # Invalidate tag lists
        for tag in post.tags.all():
            for page in range(1, 4):
                self.invalidate_fragment('tag_posts', tag.slug, page)
    
    def invalidate_user_fragments(self, user):
        """Invalidate all fragments related to a user."""
        fragments_to_invalidate = [
            ('user_profile', [user.id]),
            ('user_dashboard', [user.id, user.last_login]),
            ('author_info', [user.id]),
        ]
        
        for fragment_name, args in fragments_to_invalidate:
            self.invalidate_fragment(fragment_name, *args)
    
    def bulk_invalidate_pattern(self, pattern):
        """Invalidate all cache keys matching a pattern (Redis-specific)."""
        try:
            from django_redis import get_redis_connection
            redis_conn = get_redis_connection("default")
            
            # Find matching keys
            keys = redis_conn.keys(f"*{pattern}*")
            
            if keys:
                # Delete in batches
                batch_size = 100
                for i in range(0, len(keys), batch_size):
                    batch = keys[i:i + batch_size]
                    redis_conn.delete(*batch)
            
            return len(keys)
        
        except ImportError:
            # Fallback for non-Redis backends
            return 0

# Usage in signals
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from .models import Post, Comment

fragment_cache = FragmentCacheManager()

@receiver(post_save, sender=Post)
def invalidate_post_cache_on_save(sender, instance, **kwargs):
    """Invalidate post-related fragments when post is saved."""
    fragment_cache.invalidate_post_fragments(instance)

@receiver(post_delete, sender=Post)
def invalidate_post_cache_on_delete(sender, instance, **kwargs):
    """Invalidate post-related fragments when post is deleted."""
    fragment_cache.invalidate_post_fragments(instance)

@receiver(post_save, sender=Comment)
def invalidate_comment_cache_on_save(sender, instance, **kwargs):
    """Invalidate comment-related fragments when comment is saved."""
    # Invalidate post fragments that include comment counts
    fragment_cache.invalidate_fragment('post_meta', instance.post.id)
    fragment_cache.invalidate_fragment('post_comments', instance.post.id)

@receiver(m2m_changed, sender=Post.tags.through)
def invalidate_tag_cache_on_change(sender, instance, action, pk_set, **kwargs):
    """Invalidate tag-related fragments when post tags change."""
    if action in ['post_add', 'post_remove', 'post_clear']:
        fragment_cache.invalidate_fragment('post_tags', instance.id)
        
        # Invalidate tag list fragments
        if pk_set:
            from .models import Tag
            for tag_id in pk_set:
                try:
                    tag = Tag.objects.get(id=tag_id)
                    fragment_cache.bulk_invalidate_pattern(f'tag_posts:{tag.slug}')
                except Tag.DoesNotExist:
                    pass

Conditional Fragment Caching

Cache Based on User Permissions

<!-- Conditional caching based on user permissions -->
{% load cache %}

<!-- Cache differently for different user types -->
{% if user.is_staff %}
    {% cache 300 admin_sidebar user.id %}
        <div class="admin-sidebar">
            <h3>Admin Tools</h3>
            <ul>
                <li><a href="{% url 'admin:index' %}">Admin Panel</a></li>
                <li><a href="{% url 'moderate_comments' %}">Moderate Comments</a></li>
                <li><a href="{% url 'site_stats' %}">Site Statistics</a></li>
            </ul>
        </div>
    {% endcache %}
{% elif user.is_authenticated %}
    {% cache 600 user_sidebar user.id user.groups.all.0.id %}
        <div class="user-sidebar">
            <h3>Your Account</h3>
            <ul>
                <li><a href="{% url 'profile' %}">Profile</a></li>
                <li><a href="{% url 'my_posts' %}">My Posts</a></li>
                <li><a href="{% url 'settings' %}">Settings</a></li>
            </ul>
        </div>
    {% endcache %}
{% else %}
    {% cache 1800 guest_sidebar %}
        <div class="guest-sidebar">
            <h3>Join Us</h3>
            <p><a href="{% url 'register' %}">Sign up</a> to start blogging!</p>
            <p>Already have an account? <a href="{% url 'login' %}">Login</a></p>
        </div>
    {% endcache %}
{% endif %}

Cache Based on Request Context

<!-- Cache based on request context -->
{% load cache %}

<!-- Cache search results with query parameters -->
{% if request.GET.q %}
    {% cache 300 search_results request.GET.q request.GET.category request.GET.page %}
        <div class="search-results">
            <h2>Search Results for "{{ request.GET.q }}"</h2>
            {% for result in search_results %}
                <div class="result">
                    <h3><a href="{{ result.get_absolute_url }}">{{ result.title }}</a></h3>
                    <p>{{ result.excerpt }}</p>
                </div>
            {% endfor %}
        </div>
    {% endcache %}
{% endif %}

<!-- Cache based on device type -->
{% if request.user_agent.is_mobile %}
    {% cache 600 mobile_navigation %}
        <nav class="mobile-nav">
            <!-- Mobile-specific navigation -->
        </nav>
    {% endcache %}
{% else %}
    {% cache 1800 desktop_navigation %}
        <nav class="desktop-nav">
            <!-- Desktop navigation -->
        </nav>
    {% endcache %}
{% endif %}

<!-- Cache based on time of day -->
{% now "H" as current_hour %}
{% if current_hour|add:0 < 12 %}
    {% cache 3600 morning_banner %}
        <div class="time-banner">Good morning! Start your day with our latest articles.</div>
    {% endcache %}
{% elif current_hour|add:0 < 18 %}
    {% cache 3600 afternoon_banner %}
        <div class="time-banner">Good afternoon! Take a break and read something interesting.</div>
    {% endcache %}
{% else %}
    {% cache 3600 evening_banner %}
        <div class="time-banner">Good evening! Wind down with our featured content.</div>
    {% endcache %}
{% endif %}

Advanced Fragment Caching Patterns

Cache Warming for Template Fragments

# management/commands/warm_fragment_cache.py
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.contrib.auth.models import User
from blog.models import Post, Category, Tag

class Command(BaseCommand):
    help = 'Warm up template fragment cache'
    
    def add_arguments(self, parser):
        parser.add_argument(
            '--fragments',
            nargs='+',
            default=['all'],
            help='Specific fragments to warm up'
        )
    
    def handle(self, *args, **options):
        fragments = options['fragments']
        
        if 'all' in fragments or 'posts' in fragments:
            self.warm_post_fragments()
        
        if 'all' in fragments or 'users' in fragments:
            self.warm_user_fragments()
        
        if 'all' in fragments or 'navigation' in fragments:
            self.warm_navigation_fragments()
        
        self.stdout.write(
            self.style.SUCCESS('Fragment cache warming completed')
        )
    
    def warm_post_fragments(self):
        """Warm up post-related fragments."""
        self.stdout.write('Warming post fragments...')
        
        # Warm popular posts
        popular_posts = Post.objects.filter(
            published=True
        ).order_by('-views')[:50]
        
        for post in popular_posts:
            # Warm post summary fragment
            cache_key = make_template_fragment_key('post_summary', [post.id, post.updated_at])
            if not cache.get(cache_key):
                # Render and cache the fragment
                context = {'post': post}
                rendered = render_to_string('fragments/post_summary.html', context)
                cache.set(cache_key, rendered, 3600)
            
            # Warm post meta fragment
            cache_key = make_template_fragment_key('post_meta', [post.id])
            if not cache.get(cache_key):
                context = {'post': post}
                rendered = render_to_string('fragments/post_meta.html', context)
                cache.set(cache_key, rendered, 7200)
        
        self.stdout.write(f'Warmed {len(popular_posts)} post fragments')
    
    def warm_user_fragments(self):
        """Warm up user-related fragments."""
        self.stdout.write('Warming user fragments...')
        
        # Warm active users
        active_users = User.objects.filter(
            is_active=True,
            last_login__isnull=False
        ).order_by('-last_login')[:100]
        
        for user in active_users:
            cache_key = make_template_fragment_key('user_profile', [user.id])
            if not cache.get(cache_key):
                context = {'user': user}
                rendered = render_to_string('fragments/user_profile.html', context)
                cache.set(cache_key, rendered, 1800)
        
        self.stdout.write(f'Warmed {len(active_users)} user fragments')
    
    def warm_navigation_fragments(self):
        """Warm up navigation fragments."""
        self.stdout.write('Warming navigation fragments...')
        
        # Warm category navigation
        categories = Category.objects.all()
        cache_key = make_template_fragment_key('category_nav', [])
        if not cache.get(cache_key):
            context = {'categories': categories}
            rendered = render_to_string('fragments/category_nav.html', context)
            cache.set(cache_key, rendered, 3600)
        
        # Warm tag cloud
        popular_tags = Tag.objects.annotate(
            post_count=models.Count('posts')
        ).order_by('-post_count')[:20]
        
        cache_key = make_template_fragment_key('tag_cloud', [])
        if not cache.get(cache_key):
            context = {'tags': popular_tags}
            rendered = render_to_string('fragments/tag_cloud.html', context)
            cache.set(cache_key, rendered, 1800)
        
        self.stdout.write('Warmed navigation fragments')

Fragment Cache Analytics

# utils/fragment_analytics.py
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
import time
import logging

logger = logging.getLogger('fragment_cache')

class FragmentCacheAnalytics:
    """Track fragment cache performance and usage."""
    
    def __init__(self):
        self.stats_key_prefix = 'fragment_stats'
    
    def record_fragment_hit(self, fragment_name, cache_key):
        """Record a fragment cache hit."""
        stats_key = f"{self.stats_key_prefix}:{fragment_name}:hits"
        try:
            cache.incr(stats_key)
        except ValueError:
            cache.set(stats_key, 1, timeout=86400)  # 24 hours
        
        logger.debug(f"Fragment cache HIT: {fragment_name} ({cache_key})")
    
    def record_fragment_miss(self, fragment_name, cache_key, render_time=None):
        """Record a fragment cache miss."""
        stats_key = f"{self.stats_key_prefix}:{fragment_name}:misses"
        try:
            cache.incr(stats_key)
        except ValueError:
            cache.set(stats_key, 1, timeout=86400)
        
        if render_time:
            time_key = f"{self.stats_key_prefix}:{fragment_name}:render_time"
            total_time = cache.get(time_key, 0)
            cache.set(time_key, total_time + render_time, timeout=86400)
        
        logger.debug(f"Fragment cache MISS: {fragment_name} ({cache_key}) - {render_time:.3f}s")
    
    def get_fragment_stats(self, fragment_name):
        """Get statistics for a specific fragment."""
        hits = cache.get(f"{self.stats_key_prefix}:{fragment_name}:hits", 0)
        misses = cache.get(f"{self.stats_key_prefix}:{fragment_name}:misses", 0)
        total_time = cache.get(f"{self.stats_key_prefix}:{fragment_name}:render_time", 0)
        
        total_requests = hits + misses
        hit_rate = (hits / total_requests * 100) if total_requests > 0 else 0
        avg_render_time = (total_time / misses) if misses > 0 else 0
        
        return {
            'fragment_name': fragment_name,
            'hits': hits,
            'misses': misses,
            'total_requests': total_requests,
            'hit_rate': hit_rate,
            'total_render_time': total_time,
            'avg_render_time': avg_render_time,
        }
    
    def get_all_fragment_stats(self):
        """Get statistics for all tracked fragments."""
        # This is a simplified implementation
        # In practice, you'd want to track fragment names separately
        fragment_names = [
            'post_summary', 'post_meta', 'post_tags', 'post_comments',
            'user_profile', 'user_dashboard', 'category_nav', 'tag_cloud'
        ]
        
        stats = []
        for fragment_name in fragment_names:
            fragment_stats = self.get_fragment_stats(fragment_name)
            if fragment_stats['total_requests'] > 0:
                stats.append(fragment_stats)
        
        return stats
    
    def reset_fragment_stats(self, fragment_name=None):
        """Reset statistics for a fragment or all fragments."""
        if fragment_name:
            keys_to_delete = [
                f"{self.stats_key_prefix}:{fragment_name}:hits",
                f"{self.stats_key_prefix}:{fragment_name}:misses",
                f"{self.stats_key_prefix}:{fragment_name}:render_time",
            ]
            cache.delete_many(keys_to_delete)
        else:
            # Reset all fragment stats (Redis-specific)
            try:
                from django_redis import get_redis_connection
                redis_conn = get_redis_connection("default")
                keys = redis_conn.keys(f"{self.stats_key_prefix}:*")
                if keys:
                    redis_conn.delete(*keys)
            except ImportError:
                pass

# Template tag for analytics
from django import template

register = template.Library()
analytics = FragmentCacheAnalytics()

@register.simple_tag
def track_fragment_cache(fragment_name, *cache_key_parts):
    """Template tag to track fragment cache usage."""
    cache_key = make_template_fragment_key(fragment_name, cache_key_parts)
    
    # Check if fragment is cached
    if cache.get(cache_key):
        analytics.record_fragment_hit(fragment_name, cache_key)
        return True
    else:
        analytics.record_fragment_miss(fragment_name, cache_key)
        return False
<!-- Using fragment analytics -->
{% load cache fragment_analytics %}

<!-- Track cache performance -->
{% track_fragment_cache "post_summary" post.id post.updated_at as is_cached %}

{% cache 3600 post_summary post.id post.updated_at %}
    <article class="post-summary">
        <!-- Post content -->
    </article>
{% endcache %}

{% if not is_cached %}
    <!-- This will only render on cache miss -->
    <script>
        console.log('Post summary rendered from database');
    </script>
{% endif %}

Template fragment caching provides excellent granular control over what gets cached in your templates. The key is identifying the right fragments to cache, implementing proper cache key strategies, and setting up effective invalidation patterns. Start with caching expensive template sections and gradually implement more sophisticated patterns like nested caching and conditional caching as your application's performance requirements evolve.