Block Caching
cacheable false turns off the full page cache for the whole page, while block HTML caching needs a lifetime and tags.
<block class="..." name="..." cacheable="false"/>Two different caches apply to a block. The full page cache stores the whole rendered page. The block_html cache stores one block's HTML on its own.
cacheable false
The cacheable="false" attribute on a layout block element marks it dynamic. The effect is not local: if one non-cacheable block appears in a page's layout, the entire page is not cached. In default.xml that means every page on the storefront. For data that varies per customer, use private content instead, which keeps the page cacheable and loads the varying part separately.
Block HTML cache
AbstractBlock::getCacheLifetime() returns null unless a cache_lifetime value is set, so blocks are not cached by default. Set it as a layout argument or override the protected method. The key comes from getCacheKeyInfo(); the Template block's version already includes the store code, the template file and the base URL. Tags come from a cache_tags value plus getIdentities() when the block implements Magento\Framework\DataObject\IdentityInterface, and the block_html group tag is added for you.
What trips people up
If anything the block's output depends on is missing from the key, one visitor's HTML is served to everyone. Add it to getCacheKeyInfo(). After changing a key, a lifetime or tags, run bin/magento cache:clean block_html, and bin/magento setup:di:compile in production mode for a new class.
Example
<?php
// app/code/Vendor/Promo/Block/CategoryPromo.php
declare(strict_types=1);
namespace Vendor\Promo\Block;
use Magento\Catalog\Model\Category;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\View\Element\Template;
class CategoryPromo extends Template implements IdentityInterface
{
public function getCategoryId(): int
{
// Passed in as a layout argument, so no request parsing here.
return (int)$this->getData('category_id');
}
/**
* Cache tags: saving this category drops the stored HTML.
*
* @return string[]
*/
public function getIdentities(): array
{
return [Category::CACHE_TAG . '_' . $this->getCategoryId()];
}
/**
* The output differs per category, so the category id joins the key.
*
* @return array
*/
public function getCacheKeyInfo(): array
{
return array_merge(parent::getCacheKeyInfo(), [$this->getCategoryId()]);
}
protected function getCacheLifetime(): int
{
return 3600;
}
}
// On the category page the block renders once, then its HTML is served from
// block_html for an hour. Saving the category in the admin invalidates the
// cat_c_<id> tag and the block renders again on the next request.When to use it
- Cache an expensive category banner for an hour and invalidate it when the category is saved.
- Find why the full page cache is storing nothing on a slow storefront.
- Vary a cached block's stored HTML by customer group.
- Replace a cacheable false block with private content so pages cache again.
More examples
Setting the lifetime from layout instead
Both routes exist and they do the same thing: getCacheLifetime() reads the cache_lifetime value from the block's data, which is where a layout argument lands. Layout keeps the number configurable per handle and per theme without touching PHP. A PHP override wins over the argument, so pick one. Run bin/magento cache:flush after editing the layout file.
<!-- app/code/Vendor/Promo/view/frontend/layout/catalog_category_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Vendor\Promo\Block\CategoryPromo"
name="vendor.category.promo"
template="Vendor_Promo::category-promo.phtml">
<arguments>
<argument name="cache_lifetime" xsi:type="number">3600</argument>
<argument name="cache_tags" xsi:type="array">
<item name="category" xsi:type="string">cat_c</item>
</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>The attribute that made a storefront uncacheable
This is the shape of the incident. One block that shows a per-customer value is marked non-cacheable in default.xml, which applies to every page, and the full page cache stops storing anything. The symptom is that the site is slow everywhere and cache hit rates read as zero, with nothing wrong in the logs. Check for it with grep -r 'cacheable="false"' app/code app/design.
<!-- The bug: a cart total in the header, placed in the global handle. -->
<!-- app/code/Vendor/Header/view/frontend/layout/default.xml -->
<referenceContainer name="header.panel">
<block class="Vendor\Header\Block\CartTotal"
name="vendor.header.cart.total"
template="Vendor_Header::cart-total.phtml"
cacheable="false"/>
</referenceContainer>
<!-- Result: no storefront page is stored in the full page cache. Every
request runs the whole layout and every block. -->
<!-- The fix: drop cacheable="false" and load the varying number as private
content, so the page stays cacheable. -->A key that forgot an input
The first version stores whichever HTML rendered first and serves it to every visitor, which is how a wholesale price ends up on a retail customer's screen. Magento\Framework\App\Http\Context holds the few values that are allowed to vary cached output, such as Magento\Customer\Model\Context::CONTEXT_GROUP; read the varying value from there rather than from the customer session. If a value cannot be reduced to a short list, the block should not be cached at all.
<?php
// Wrong: the output depends on the customer group, the key does not.
public function getCacheKeyInfo(): array
{
return parent::getCacheKeyInfo();
}
// Right: every input that changes the HTML belongs in the key.
public function getCacheKeyInfo(): array
{
return array_merge(
parent::getCacheKeyInfo(),
[
$this->getCategoryId(),
$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP),
]
);
}
Discussion