How Block HTML Cache works in Magento 2

The Block HTML Cache works in combination with the layout cache.

If you check the Cache Management page on admin panel, you may notice there is a "Blocks HTML Output", this configuration controls availability of Block HTML Cache.

Magento 2 mainly makes use of full page cache and is compatible with Varnish and Fastly CDN.

In this article we focus on how Block HTML Cache works.

The Block HTML Cache works in combination with the layout cache. So let's see how layout cache works first.

<?php
# File: Magento\Framework\View\Layout
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 saves and loads the layout(XML) cache and is used at page building stage. The Block HTML Cache happens at next stage.

<?php
# File: Magento\Framework\View\Layout
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 output in memory. It increases speed if a block is needed to be rendered more than one time.

Now the most important part of this topic:

<?php
# File: Magento\Framework\View\Element\AbstractBlock
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
    );
}

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 method checks that if the block cache exists, then use it, if not, call renderNonCachedElement and cache the result.

cache tags are also saved and they are used by generating full page cache latter.

The default cache lifetime for blocks is 0, which means by default Block HTML is not cached.

However, Block HTML Cache only has small performance improvement and is hard to use compared to the full page cache.