How Block HTML Cache works in Magento 2

You may notice there is a Blocks HTML Output on the Admin Panel Cache Management page. This type of cache is different from the Full Page Cache and you may be not familiar. Usually, it is called "Block HTML Cache" or simply "Block Cache". In this blog post we focus on how Block Cache works.

The Block Cache works in combination with the "Layout Cache". So let's see how Layout Cache works first.

Recommended Extension

Layout Cache

\Magento\Framework\View\Layout::generateElements

/**
 * Create structure of elements from the loaded XML configuration
 *
 * @return void
 */
public function generateElements()
{
    \Magento\Framework\Profiler::start(__CLASS__ . '::' . __METHOD__);
    $cacheId = 'structure_' . $this->getUpdate()->getCacheId();
    $result = $this->cache->load($cacheId);
    if ($result) {
        $data = $this->serializer->unserialize($result);
        $this->getReaderContext()->getPageConfigStructure()->populateWithArray($data['pageConfigStructure']);
        $this->getReaderContext()->getScheduledStructure()->populateWithArray($data['scheduledStructure']);
    } else {
        \Magento\Framework\Profiler::start('build_structure');
        $this->readerPool->interpret($this->getReaderContext(), $this->getNode());
        \Magento\Framework\Profiler::stop('build_structure');

        $data = [
            'pageConfigStructure' => $this->getReaderContext()->getPageConfigStructure()->__toArray(),
            'scheduledStructure'  => $this->getReaderContext()->getScheduledStructure()->__toArray(),
        ];
        $handles = $this->getUpdate()->getHandles();
        $this->cache->save($this->serializer->serialize($data), $cacheId, $handles, $this->cacheLifetime);
    }

    $generatorContext = $this->generatorContextFactory->create(
        [
            'structure' => $this->structure,
            'layout' => $this,
        ]
    );

    \Magento\Framework\Profiler::start('generate_elements');
    $this->generatorPool->process($this->getReaderContext(), $generatorContext);
    \Magento\Framework\Profiler::stop('generate_elements');

    $this->addToOutputRootContainers();
    \Magento\Framework\Profiler::stop(__CLASS__ . '::' . __METHOD__);
}

This method tries to load the layout structure from cache and if the structure does not exist, it will get the structure from merged layout XML and save the result to Layout Cache for next use. This method is called at page building stage. The Block Cache will be loaded at next stage.

Block Cache

\Magento\Framework\View\Layout::renderElement

/**
 * Find an element in layout, render it and return string with its output
 *
 * @param string $name
 * @param bool $useCache
 * @return string
 */
public function renderElement($name, $useCache = true)
{
    $this->build();
    if (!isset($this->_renderElementCache[$name]) || !$useCache) {
        if ($this->displayElement($name)) {
            $this->_renderElementCache[$name] = $this->renderNonCachedElement($name);
        } else {
            return $this->_renderElementCache[$name] = '';
        }
    }
    $this->_renderingOutput->setData('output', $this->_renderElementCache[$name]);
    $this->_eventManager->dispatch(
        'core_layout_render_element',
        ['element_name' => $name, 'layout' => $this, 'transport' => $this->_renderingOutput]
    );
    return $this->_renderingOutput->getData('output');
}

This method in the same file caches rendered HTML output in memory. It increases performance if a block is needed to be rendered more than once.

The Core Part

\Magento\Framework\View\Element\AbstractBlock::_loadCache

/**
 * Load block html from cache storage
 *
 * @return string
 */
protected function _loadCache()
{
    $collectAction = function () {
        if ($this->hasData('translate_inline')) {
            $this->inlineTranslation->suspend($this->getData('translate_inline'));
        }

        $this->_beforeToHtml();
        return $this->_toHtml();
    };

    if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) {
        $html = $collectAction();
        if ($this->hasData('translate_inline')) {
            $this->inlineTranslation->resume();
        }
        return $html;
    }
    $loadAction = function () {
        return $this->_cache->load($this->getCacheKey());
    };

    $saveAction = function ($data) {
        $this->_saveCache($data);
        if ($this->hasData('translate_inline')) {
            $this->inlineTranslation->resume();
        }
    };

    return (string)$this->lockQuery->lockedLoadData(
        $this->getCacheKey(),
        $loadAction,
        $collectAction,
        $saveAction
    );
}

\Magento\Framework\View\Element\AbstractBlock::_saveCache

/**
 * Save block content to cache storage
 *
 * @param string $data
 * @return $this
 */
protected function _saveCache($data)
{
    if (!$this->getCacheLifetime() || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) {
        return false;
    }
    $cacheKey = $this->getCacheKey();

    $this->_cache->save($data, $cacheKey, array_unique($this->getCacheTags()), $this->getCacheLifetime());
    return $this;
}

This pair of methods works very similar to the Layout Cache introduced in the previous section. The logic is first try to load Block Cache. If it exists, use it, otherwise call \Magento\Framework\View\Layout::renderNonCachedElement and cache the result for next use.

BTW, what are "Cache Tags"?

In short, they are "identities" for Full Page Cache. Read this blog post for a full understanding.

How to let my block be cached?

Check this part in \Magento\Framework\View\Element\AbstractBlock::_saveCache again.

if (!$this->getCacheLifetime() || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) {
    return false;
}

So in order to let the block be cached, the block must have a positive cache lifetime.

What's the default cache lifetime for a block?

If you have ever done a customization, you should already know custom block must extend \Magento\Framework\View\Element\Template( or \Magento\Framework\View\Element\AbstractBlock).

Let's see the base getCacheLifetime method.

\Magento\Framework\View\Element\AbstractBlock::getCacheLifetime

/**
 * Get block cache life time
 *
 * @return int|bool|null
 */
protected function getCacheLifetime()
{
    if (!$this->hasData('cache_lifetime')) {
        return null;
    }

    $cacheLifetime = $this->getData('cache_lifetime');
    if (false === $cacheLifetime || null === $cacheLifetime) {
        return null;
    }

    return (int)$cacheLifetime;
}

So the default cache lifetime for a block is 0(NULL), which means by default a block won't be cached.

There are 2 ways to change this behavior.

Recommended Way

Add argument to the block in layout XML.

<block class="TheBlock" name="the_name">
    <arguments>
        <argument name="cache_lifetime" xsi:type="number">3600</argument><!-- 1 hour -->
    </arguments>
</block>

Alternative Way

Override the block's getCacheLifetime method.

/**
 * @inheritdoc
 */
protected function getCacheLifetime()
{
    return 60 * 60; // 1 hour
}

Final Thoughts

However, in most situations, Block Cache is hard to use and only has very limited performance boost compared to the Full Page Cache.

If your project only has a small budget, focus on the Full Page Cache.

If you found this blog post helpful, please share it!