silverstripe-framework/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md

8.6 KiB

title summary icon
Partial Caching Cache SilverStripe templates to reduce database queries. tachometer-alt

Partial Caching

Partial caching is a feature that allows the caching of just a portion of a page.

<% cached 'CacheKey' %>
    $DataTable
    ...
<% end_cached %>

Each cache block has a cache key. A cache key is an unlimited number of comma separated variables and quoted strings. Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is the same as a previous render, the cached value stored last time is used.

Since the above example contains just one argument as the cache key, a string (which will be the same every render) it will invalidate the cache after a given amount of time has expired (default 10 minutes).

Here are some more complex examples:

<% cached 'database', $LastEdited %> 
    <!-- that updates every time the record changes. -->
<% end_cached %>

<% cached 'loginblock', $CurrentMember.ID %>
    <!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
<% end_cached %>

<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
    <!-- recached when block object changes, and if the user is admin -->
<% end_cached %>

An additional global key is incorporated in the cache lookup. The default value for this is $CurrentReadingMode, $CurrentUser.ID. This ensures that the current Versioned state and user ID are used. This may be configured by changing the config value of SSViewer.global_key. It is also necessary to flush the template caching when modifying this config, as this key is cached within the template itself.

For example, to ensure that the cache is configured to respect another variable, and if the current logged in user does not influence your template content, you can update this key as below;

app/_config/app.yml

SilverStripe\View\SSViewer:
  global_key: '$CurrentReadingMode, $Locale'

Aggregates

Often you want to invalidate a cache when any object in a set of objects change, or when the objects in a relationship change. To do this, SilverStripe introduces the concept of Aggregates. These calculate and return SQL aggregates on sets of DataObjects - the most useful for us being the max aggregate.

For example, if we have a menu, we want that menu to update whenever any page is edited, but would like to cache it otherwise. By using aggregates, we do that like this:

<% cached 'navigation', $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), $List('SilverStripe\CMS\Model\SiteTree').count() %>

The cache for this will update whenever a page is added, removed or edited. (Note: The use of the fully qualified classname is necessary).

If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added or edited:

<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>

[notice] Note the use of both .max('LastEdited') and .count() - this takes care of both the case where an object has been edited since the cache was last built, and also when an object has been deleted since the cache was last built. [/notice]

We can also calculate aggregates on relationships. The logic for that can get a bit complex, so we can extract that on to the controller so it's not cluttering up our template.

Cache key calculated in controller

If your caching logic is complex or re-usable, you can define a method on your controller to generate a cache key fragment.

For example, a block that shows a collection of rotating slides needs to update whenever the relationship Page::$many_many = ['Slides' => 'Slide'] changes. In PageController:

public function SliderCacheKey() 
{
    $fragments = [
        'Page-Slides',
        $this->ID,
        // identify which objects are in the list and their sort order
        implode('-', $this->Slides()->Column('ID')),
        $this->Slides()->max('LastEdited')
    ];
    return implode('-_-', $fragments);
}

Then reference that function in the cache key:

<% cached $SliderCacheKey %>

The example above would work for both a has_many and many_many relationship.

Cache blocks and template changes

In addition to the key elements passed as parameters to the cached control, the system automatically includes the template name and a sha1 hash of the contents of the cache block in the key. This means that any time the template is changed the cached contents will automatically refreshed.

Purposely stale data

In some situations it's more important to be fast than to always be showing the latest data. By constructing the cache key to invalidate less often than the data updates you can ensure rendering time is constant no matter how often the data updates.

For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do

<% cached 'blogstatistics', $Blog.ID %>

which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is configurable only on a site-wide basis), you could add a special function to your controller:

public function BlogStatisticsCounter() 
{
    return (int)(time() / 60 / 5); // Returns a new number every five minutes
}

and then use it in the cache key

<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>

Cache block conditionals

You may wish to conditionally enable or disable caching. To support this, in cached tags you may (after any key arguments) specify 'if' or 'unless' followed by a standard template variable argument. If 'if' is used, the resultant value must be true for that block to be cached. Conversely if 'unless' is used, the result must be false.

Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing heavy load:

<% cached 'blogstatistics', $Blog.ID if $HighLoad %>

By adding a HighLoad function to your PageController, you could enable or disable caching dynamically.

To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members, use something like:

<% cached unless $CurrentUser %>

Uncached

The template tag 'uncached' can be used - it is the exact equivalent of a cached block with an if condition that always returns false. The key and conditionals in an uncached tag are ignored, so you can easily temporarily disable a particular cache block by changing just the tag, leaving the key and conditional intact.

<% uncached %>

Nested cache blocks

You can also nest independent cache blocks Any nested cache blocks are calculated independently from their containing block, regardless of the cached state of that container.

This allows you to wrap an entire page in a cache block on the page's LastEdited value, but still keep a member-specific portion dynamic, without having to include any member info in the page's cache key.

An example:

<% cached $LastEdited %>
    Our wonderful site
    
    <% cached $Member.ID %>
        Welcome $Member.Name
    <% end_cached %>
    
    $ASlowCalculation
<% end_cached %>

This will cache the entire outer section until the next time the page is edited, but will display a different welcome message depending on the logged in member.

Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you could also write the last example as:

<% cached $LastEdited %>
    Our wonderful site
    
    <% uncached %>
        Welcome $Member.Name
    <% end_uncached %>
    
    $ASlowCalculation
<% end_cached %>

[warning] Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error letting you know if you've done this. You can often get around this using aggregates or by un-nesting the block. [/warning]

Failing example:

<% cached $LastEdited %>

    <% loop $Children %>
        <% cached $LastEdited %>
            $Name
        <% end_cached %>
    <% end_loop %>

<% end_cached %>

Can be re-written as:

<% cached $LastEdited %>
    <% cached $AllChildren.max('LastEdited') %>
        <% loop $Children %>
            $Name
        <% end_loop %>
    <% end_cached %>
<% end_cached %>

Or:

<% cached $LastEdited %>
    (other code)
<% end_cached %>

<% loop $Children %>
    <% cached $LastEdited %>
      $Name
    <% end_cached %>
<% end_loop %>