diff --git a/docs/en/02_Developer_Guides/01_Templates/07_Caching.md b/docs/en/02_Developer_Guides/01_Templates/07_Caching.md index 7736f7c90..879b94b35 100644 --- a/docs/en/02_Developer_Guides/01_Templates/07_Caching.md +++ b/docs/en/02_Developer_Guides/01_Templates/07_Caching.md @@ -1,6 +1,6 @@ --- title: Caching -summary: Reduce rendering time with cached templates and understand the limitations of the ViewableData object caching. +summary: How template variables are cached. icon: rocket --- @@ -35,13 +35,11 @@ When we render `$Counter` to the template we would expect the value to increase ## Partial caching -Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data -from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching). +Partial caching is a feature that allows caching of a portion of a page as a single string value. For more details read [its own documentation](partial_template_caching). +Example: ```ss -<% cached 'MyCachedContent', LastEdited %> - $Title +<% cached $CacheKey if $CacheCondition %> + $CacheableContent <% end_cached %> ``` - - diff --git a/docs/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md b/docs/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md new file mode 100644 index 000000000..c6a55195d --- /dev/null +++ b/docs/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md @@ -0,0 +1,335 @@ +--- +title: Partial Template Caching +summary: Cache a section of a template Reduce rendering time with cached templates and understand the limitations of the ViewableData object caching. +icon: tags +--- + +## Partial template caching + +Partial template caching is a feature that allows caching of rendered portions of templates. Cached content +is fetched from a [cache backend](../performance/caching), instead of being regenerated repeatedly. + + +### Base syntax + +```ss +<% cached $CacheKey if $CacheCondition %> + $CacheableContent +<% end_cached %> +``` + +This is not a definitive example of the syntax, but it shows the most common use case. + +[note] +See also [Complete Syntax definition](#complete-syntax-defintition) section +[/note] + +The key parts are `$CacheKey`, `$CacheCondition` and `$CacheableContent`. +The following sections explain every one of them in more detail. + + +#### $CacheKey + +Defines a unique key for the cache storage. + +[warning] +Avoid heavy computations in `$CacheKey` as it is evaluated for every template render. +[/warning] + +The formal definition is + - Optional list of template expressions delimited by comma + +The syntax is + +```ss +<% cached [$key1[, $key2[, ...[, $keyN]]]] ... %> +``` + +The final value is concatenated by the Template Engine into a string. When doing so, Template Engine +adds some extra values to the mix to make it more unique and prevent clashing between cache keys from +different templates. + +Here is how it works in detail: + +1. `SilverStripe\View\SSViewer::$global_key` hash + + With the current template context, value of the `$global_key` variable is rendered into a string and hashed. + + `$global_key` content is inserted into the template "as is" at the compilation stage. Changing its value + won't have any effect until template recompilation (e.g. on cache flush). + + By default it equals to `'$CurrentReadingMode, $CurrentUser.ID'`. + This ensures the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used. + At runtime that will become something like `'LIVE, 0'` (for unauthenticated users in live mode). + + As usual, you may override its value via YAML configs. For example: + + ```yml + # app/_config/view.yml + SilverStripe\View\SSViewer: + global_key: '$CurrentReadingMode, $CurrentUser.ID, $Locale' + ``` + +2. Block hash + + Everything between the `<% cached %> ... <% end_cached %>` is taken as text (with no rendering) and hashed. + + This is done at the template compilation stage, so + the compiled version of the template contains the hash precalculated. + + `Block hash` main purpose is to invalidate cache when template itself changes. + +3. `$CacheKey` hash + + All keys of `$CacheKey` are processed, concatenated and the final value is hashed. + If there are no values defined, this step is skipped. + +4. Make the final key vaule + + A string produced by concatenation of all the values mentioned above is used as the final value. + + Even if `$CacheKey` is omitted, `SilverStripe\View\SSViewer::$global_key` and `Block hash` values are still + getting used to generate cache key for the caching backend storage. + +[note] +##### 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`: + + +```php +public function SliderCacheKey() +{ + $fragments = [ + 'Page-Slides', + $this->ID, + // identify which objects are in the list and their sort order + implode('-', $this->Slides()->Column('ID')), + // works for both has_many and many_many relationships + $this->Slides()->max('LastEdited') + ]; + return implode('-_-', $fragments); +} +``` + +Then reference that function in the cache key: + + +```ss +<% cached $SliderCacheKey if ... %> +``` +[/note] + + +#### $CacheCondition + +Defines if caching is required for the block. + +Condition is optional and if omitted, `true` is implied. + +If the value is `false`, the block skips `$CacheKey` evaluation completely, does not lookup +the data in the cache storage, neither preserve any data in the storage. +The template within the block keeps working as is, same as it would do without +`<% cached %>` block surrounding it. + +Although `$CacheCondition` is optional, it is highly recommended. For example, +if you use `$DataObject->ID` as your `$CacheKey`, you may use +`$DataObject->ID > 0` as the condition. + +Without it: + - your cache backend will always be queried for cache (for every template render) + - your cache backend may be cluttered with redundant and useless data + + +[warning] +The `$CacheCondition` value is evaluated on every template render and should be as lightweight as possible. +[/warning] + + +#### $CacheableContent + +The content block may contain any usual template syntax. + + +### Cache storage + +The cache storage may be re-configured via `Psr\SimpleCache\CacheInterface.cacheblock` key for [Injector](../extending/injector). +By default, it is initialised by `SilverStripe\Core\Cache\DefaultCacheFactory` with the following parameters: + +- `namespace: "cacheblock"` +- `defaultLifetime: 600` + +[note] +The defaultLifetime 600 means every cache record expires in 10 minutes. +If you have good `$CacheKey` and `$CacheCondition` implementations, you may want to tune these settings to +improve performance. +[/note] + + +### Nested cached blocks + +Every nested cache block is processed independently. + +Let's consider the following example: +```ss +<% cached $PageKey %> + + <% cached $BodyKey %> <% end_cached %> + +<% end_cached %> +``` + +The template processor will transparently flatten the structure into something similar to the following pseudo-code: + +```ss +<% cached $PageKey %><% end_block %> +<% cached $BodyKey %><% end_cached %> +<% cached $PageKey %><% end_cached %> +``` + +[note] +`$PageKey` is used twice, but evaluated only once per render because of [template object caching](caching/#object-caching). +[/note] + + +### Uncached + +The tag `<% uncached %> ... <% end_uncached %>` disables caching for its content. + +```ss +<% cached $PageKey %> + + <% uncached %><% end_uncached %> + +<% end_cached %> +``` + +Because of the nested block flattening (see above), it works seamlessly on any level of depth. + +[warning] +The `uncached` block only works on the lexical level. +If you have a template that caches content rendering another template with included uncached blocks, +those will not have any effect on the parent template caching blocks. +[/warning] + + +### Nesting in FOR and IF blocks + +Currently, a cache block cannot be included in `if` and `loop` blocks. +The template engine will throw an error letting you know if you've done this. + +[note] +You may often get around this using aggregates or by un-nesting the block. + +E.g. + +``` +<% cached $LastEdited %> + <% loop $Children %> + <% cached $LastEdited %> + $Name + <% end_cached %> + <% end_loop %> +<% end_cached %> +``` + +Might be re-written as something like that: + +``` +<% cached $LastEdited %> + <% cached $AllChildren.max('LastEdited') %> + <% loop $Children %> + $Name + <% end_loop %> + <% end_cached %> +<% end_cached %> +``` +[/note] + + +### Unless (syntax sugar) + +`if` keyword may be swapped with keyword `unless`, which inverts the boolean value evaluation. + +The two following forms produce the same result + +```ss +<% cached unless $Key %> + "unless $Cond" === "if not $Cond" +<% end_cached %> +``` + +```ss +<% cached if not $Key %> + "unless $Cond" === "if not $Cond" +<% end_cached %> +``` + + +### Complete Syntax definition + +```ss +<% [un]cached [$CacheKey[, ...]] [(if|unless) $CacheCondition] %> + $CacheContent +<% end_[un]cached %> +``` + + +### Examples + +```ss +<% cached %> + The key is: hash of the template code within the block with $global_key. + This content is always cached. +<% end_cache %> +``` + +```ss +<% cached $Key %> + Cached separately for every distinct $Key value +<% end_cached %> +``` + +```ss +<% cached $KeyA, $KeyB %> + Cached separately for every combination of $KeyA and $KeyB +<% end_cached %> +``` + +```ss +<% cached $Key if $Cond %> + Cached only if $Cond == true +<% end_cached %> +``` + +```ss +<% cached $Key unless $Cond %> + Cached only if $Cond == false +<% end_cached %> +``` + +```ss +<% cached $Key if not $Cond %> + Cached only if $Cond == false +<% end_cached %> +``` + +```ss +<% cached 'contentblock', $LastEdited, $CurrentMember.ID if $CurrentMember && not $CurrentMember.isAdmin %> + + <% uncached %> + This text is always dynamic (never cached) + <% end_uncached %> + +<% end_cached %> +``` diff --git a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index c07a636fd..da4a3ea50 100644 --- a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -6,124 +6,107 @@ icon: tachometer-alt # Partial Caching -Partial caching is a feature that allows the caching of just a portion of a page. +[Partial template caching](../templates/partial_template_caching) is a feature that allows caching of rendered portions a template. + + +## Cache block conditionals + +Use conditions whenever possible. The cache tag supports defining conditions via either `if` or `unless` keyword. +Those are optional, however is highly recommended. + +[warning] +Avoid performing heavy computations in conditionals, as they are evaluated for every template rendering. +[/warning] + +If you cache without conditions: + - your cache backend will always be queried for the cache block (on every template render) + - your cache may be cluttered with heaps of redundant and useless data (especially the default filesystem backend) + +As an example, if you use `$DataObject->ID` as a key for the block, consider adding a condition that ID is greater than zero: ```ss -<% cached 'CacheKey' %> - $DataTable - ... -<% end_cached %> +<% cached $MenuItem.ID if $MenuItem.ID > 0 %> ``` -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: - +To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members, + use something like: ```ss -<% cached 'database', $LastEdited %> - -<% end_cached %> - -<% cached 'loginblock', $CurrentMember.ID %> - -<% end_cached %> - -<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %> - -<% end_cached %> +<% cached unless $CurrentUser %> ``` -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](api:SilverStripe\Versioned\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** - - -```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 [DataObject](api:SilverStripe\ORM\DataObject)s - the most useful for us being the `max` aggregate. +Sometimes you may want to invalidate cache when any object in a set changes, or when objects in a relationship +change. To do this, you may use [DataList](api:SilverStripe\ORM\DataList) aggregate methods (which we call Aggregates). +These perform SQL aggregate queries on sets of [DataObject](api:SilverStripe\ORM\DataObject)s. -For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it +Here are some useful methods of the [DataList](api:SilverStripe\ORM\DataList) class: + - `int count()` : Return the number of items in this DataList + - `mixed max(string $fieldName)` : Return the maximum value of the given field in this DataList + - `mixed min(string $fieldName)` : Return the minimum value of the given field in this DataList + - `mixed avg(string $fieldName)` : Return the average value of the given field in this DataList + - `mixed sum(string $fieldName)` : Return the sum of the values of the given field in this DataList + +To construct a `DataList` over a `DataObject`, we have a global template variable called `$List`. + +For example, if we have a menu, we may 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: +```ss +<% 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. +[/note] + +[note] +The use of both `.max('LastEdited')` and `.count()` makes sure we check for any object +edited or deleted since the cache was last built. +[/note] + +[warning] +Be careful using aggregates. Remember that the database is usually one of the performance bottlenecks. +Keep in mind that every key of every cached block is recalculated for every template render, regardless of caching +result. Aggregating SQL queries are usually produce more load on the database than simple select queries, +especially if you query records by Primary Key or join tables using database indices properly. + +Sometimes it may be cheaper to not cache altogether, rather than cache a block using a bunch of heavy aggregating SQL +queries. + +Let us consider two versions: ```ss -<% cached 'navigation', $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), $List('SilverStripe\CMS\Model\SiteTree').count() %> +# Version 1 (bad) + +<% cached + $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), + $List('SilverStripe\CMS\Model\SiteTree').count() +%> + Parent title is: $Me.Parent.Title +<% end_cached %> ``` -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: - - ```ss -<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %> +# Version 2 (better performance than Version 1) + +Parent title is: $Me.Parent.Title ``` -[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] +`Version 1` always generates two heavy aggregating SQL queries for the database on every +template render. +`Version 2` always generates a single and more performant SQL query fetching the record by its Primary Key. -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. +[/warning] -## 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`: - - -```php -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: - - -```ss -<% 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 @@ -149,7 +132,7 @@ public function BlogStatisticsCounter() } ``` - + and then use it in the cache key @@ -157,126 +140,36 @@ 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. +## Cache backend -Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing -heavy load: +The template engine uses [Injector](../extending/injector) service `Psr\SimpleCache\CacheInterface.cacheblock` as +caching backend. The default definition of that service is very conservative and relies on the server filesystem. +This is the most common denominator for most of the applications out there. However, +this is not the most robust neither performant cache implementation. If you have a better solution +available on your platform, you should consider tuning that setting for your application. +All you need to do to swap the cache backend for partial template cache blocks is to redefine this service for the Injector. +Here's an example of how it could be done: -```ss -<% cached 'blogstatistics', $Blog.ID if $HighLoad %> +```yml +# app/_config/cache.yml + +--- +Name: app-cache +After: + - 'corecache' +--- +SilverStripe\Core\Injector\Injector: + Psr\SimpleCache\CacheInterface.cacheblock: '%$App\Cache\Service.memcached' ``` -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: - - -```ss -<% 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. - - -```ss -<% 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: - - -```ss -<% 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: - - -```ss -<% cached $LastEdited %> - Our wonderful site - - <% uncached %> - Welcome $Member.Name - <% end_uncached %> - - $ASlowCalculation -<% end_cached %> -``` +[note] +For the above example to work it is necessary to have the Injector service `App\Cache\Service.memcached` defined somewhere in the configs. +[/note] [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. +The default filesystem cache backend does not support auto cleanup of the residual files with expired cache records. +If your project relies on Template Caching heavily (e.g. thousands of cache records daily), you may want to keep en eye on the +filesystem storage. Sooner or later its capacity may be exhausted. [/warning] - -Failing example: - - -```ss -<% cached $LastEdited %> - - <% loop $Children %> - <% cached $LastEdited %> - $Name - <% end_cached %> - <% end_loop %> - -<% end_cached %> -``` - -Can be re-written as: - - -```ss -<% cached $LastEdited %> - <% cached $AllChildren.max('LastEdited') %> - <% loop $Children %> - $Name - <% end_loop %> - <% end_cached %> -<% end_cached %> -``` - -Or: - -```ss -<% cached $LastEdited %> - (other code) -<% end_cached %> - -<% loop $Children %> - <% cached $LastEdited %> - $Name - <% end_cached %> -<% end_loop %> -```