Caching dynamic web pages (page may be 99% static but contain some dynamic content)

Learn caching dynamic web pages (page may be 99% static but contain some dynamic content) with practical examples, diagrams, and best practices. Covers javascript, php, python development technique...

Optimizing Performance: Caching Dynamic Web Pages with Mixed Content

Hero image for Caching dynamic web pages (page may be 99% static but contain some dynamic content)

Learn effective strategies for caching web pages that are mostly static but contain dynamic elements, improving performance and user experience across various programming languages.

In modern web development, many pages are not entirely static or entirely dynamic. Often, a significant portion of a page's content remains constant, while small, personalized sections (like a user's name, shopping cart count, or real-time updates) change frequently. Caching such '99% static, 1% dynamic' pages presents a unique challenge: how do you leverage the benefits of caching for the static parts without serving stale dynamic content? This article explores various techniques and best practices for intelligently caching these mixed-content pages, ensuring both performance and data freshness.

Understanding the Caching Challenge

Traditional full-page caching works well for purely static pages, storing the entire HTML output and serving it directly. For purely dynamic pages, caching might be applied at the data layer or through fragment caching. However, when a page combines both, a naive full-page cache would either serve outdated dynamic content or be bypassed entirely, negating its benefits. The goal is to cache the static shell of the page and dynamically inject or update the personalized components.

flowchart TD
    A[User Request] --> B{Page in Cache?}
    B -- No --> C[Generate Full Page]
    C --> D[Store Static Shell in Cache]
    D --> E[Serve Page to User]
    B -- Yes --> F{Dynamic Content Present?}
    F -- No --> E
    F -- Yes --> G[Serve Cached Static Shell]
    G --> H[Fetch Dynamic Content (AJAX/ESI)]
    H --> I[Inject/Update Dynamic Content]
    I --> E

Caching workflow for pages with mixed static and dynamic content.

Strategies for Mixed-Content Caching

Several strategies can be employed to effectively cache pages with dynamic elements. The choice depends on the complexity of the dynamic parts, the server-side technology, and the desired level of caching granularity.

1. Client-Side Rendering with AJAX

This is one of the most common and flexible approaches. The server renders a mostly static HTML page, which is then cached. Dynamic content areas are left empty or contain loading indicators. Once the page loads in the browser, JavaScript makes AJAX requests to fetch the dynamic data, which is then injected into the appropriate DOM elements. This offloads the dynamic content generation to separate, smaller API endpoints that can be cached independently or not at all.

<!DOCTYPE html>
<html>
<head>
    <title>My Dynamic Page</title>
</head>
<body>
    <h1>Welcome to our site!</h1>
    <div id="user-greeting">Loading user data...</div>
    <p>This is static content that can be cached.</p>
    <div id="realtime-updates"></div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // Fetch user greeting
            fetch('/api/user/greeting')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('user-greeting').innerText = 'Hello, ' + data.name + '!';
                })
                .catch(error => console.error('Error fetching user greeting:', error));

            // Fetch real-time updates
            setInterval(() => {
                fetch('/api/updates')
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('realtime-updates').innerText = 'Latest update: ' + data.message;
                    })
                    .catch(error => console.error('Error fetching updates:', error));
            }, 5000); // Update every 5 seconds
        });
    </script>
</body>
</html>

HTML structure with placeholders for dynamic content and client-side AJAX calls.

2. Server-Side Includes (SSI) or Edge Side Includes (ESI)

SSI and ESI allow you to define 'holes' in your static HTML content that are filled with dynamic content at the server or edge caching layer. SSI is typically handled by the web server (e.g., Nginx, Apache), while ESI is a more powerful standard often used by CDNs or specialized ESI processors. The main page can be cached, and the included dynamic fragments are fetched separately and inserted before serving the page to the user. This keeps the dynamic logic off the main application server for cached requests.

<!DOCTYPE html>
<html>
<head>
    <title>My Dynamic Page</title>
</head>
<body>
    <h1>Welcome to our site!</h1>
    <!--#include virtual="/dynamic-greeting.html" -->
    <p>This is static content that can be cached.</p>
    <!--#include virtual="/dynamic-updates.html" -->
</body>
</html>

Example of an HTML page using Server-Side Includes (SSI) directives.

3. Fragment Caching (Server-Side)

Many web frameworks (like Ruby on Rails, Django, Laravel, and some PHP frameworks) offer built-in fragment caching. This allows you to cache specific blocks or 'fragments' of a page's HTML output. The main page template is processed, but when it encounters a cached fragment, it retrieves it directly from the cache instead of re-rendering. This is effective when dynamic parts are well-defined and encapsulated within the server-side rendering process.

Ruby on Rails

<% cache 'user_greeting_partial' do %>

Hello, <%= current_user.name %>!
<% end %>

Static content here.

<% cache ['realtime_updates', Time.now.to_i / 60] do %>

<%= render 'shared/realtime_updates' %>
<% end %>

PHP (Laravel Blade)

@cache('user_greeting_partial')

Hello, {{ Auth::user()->name }}!
@endcache

Static content here.

@cache('realtime_updates', 60) {{-- Cache for 60 seconds --}}

@include('partials.realtime_updates')
@endcache

Python (Django)

{% load cache %}

{% cache 3600 user_greeting_partial %}

Hello, {{ request.user.username }}!
{% endcache %}

Static content here.

{% cache 60 realtime_updates %}

{% include 'partials/realtime_updates.html' %}
{% endcache %}

4. Cache Invalidation and Keying

Regardless of the strategy, effective cache invalidation is critical. For dynamic content, ensure that the cache is cleared or updated when the underlying data changes. For fragment caching, consider using cache keys that incorporate relevant data identifiers or timestamps to ensure freshness. For example, a user's profile fragment might be keyed by user_id and a version number derived from their last update time.

1. Identify Dynamic Zones

Carefully analyze your web page to pinpoint exactly which sections contain dynamic, user-specific, or frequently changing content. Mark these areas for special handling.

2. Choose a Strategy

Based on your application's architecture, framework, and the nature of the dynamic content, select the most appropriate caching strategy (AJAX, ESI/SSI, or fragment caching).

3. Implement Caching Logic

Apply the chosen caching mechanism. This might involve adding JavaScript for AJAX, configuring your web server for ESI/SSI, or using your framework's built-in fragment caching features.

4. Set Up Invalidation

Establish clear rules and mechanisms for invalidating or updating cached content when the underlying dynamic data changes. This is crucial to prevent serving stale information.

5. Monitor and Optimize

After deployment, monitor your page load times and cache hit rates. Adjust cache durations and strategies as needed to find the optimal balance between performance and data freshness.