Why are some blocks stored in cache but never fetched?
Back to basics
When you render an HTML block, by yourself using the layout component and calling its toHtml() or the XML layout files,
Magento will - if the Block HTML cache is enabled - get its cache lifetime. If that lifetime is strictly positive,
Magento will get its cache key and ask the cache backend (var/cache, memcached, Redis, …) for the stored text.
If the cache backend doesn’t have that key, or if that key expired, the _toHtml() method will be called, and your template
will be executed if you didn’t override the _toHtml(). If your block embed child blocks - and it does, most of the time -,
the process will be repeated for that child.
However, if the cache backend has non-expired data for that key, the HTML will be directly returned and its _toHtml()
won’t be run (and by extension the phtml template won’t be included). Thus, the children will never be instantiated if
the parent block is stored in the cache.
What would happen if all your blocks were cacheable?
Let suppose we have a block A, that has three children, B, C and D.
We also have a block Q which has three children too: R and S, but also D, as A does.
A is used on page P1, and B on page P2. B, C, D, R and S are never used elsewhere.
If your cache are cold, if you open P1 :
A is not in the cache, so we generate that block
B is not in the cache, so we generate that block too, and cache it
C is not in the cache, so we generate that block too, and cache it too
D is not in the cache, so we generate that block too, and cache it too
we save A in the cache
If you open P1 again :
A is stored in the cache, Magento returns it. B, C and D were not used!
If you open P2 :
Q is not in the cache, so we generate that block
R is not in the cache, so we generate that block too, and cache it
S is not in the cache, so we generate that block too, and cache it too
D is in the cache, Magento returns it
we save Q in the cache
So we can identify three cases:
A will be loaded every time P1 is opened, and Q every time P2 is opened (unless full page cache is active), so they deserve to be cached;
B, C, R and S will never be used: their content is embedded in their parent block. As they are no other uses, there is no reason to cache them as they pollute the cache.
D won’t be fetched once both A and Q are loaded. It may or may not be useful to cache it, according to the time you gain, the amount of data you store and its lifetime.
Why is it a problem?
Having unused data in the cache is not a serious problem, however it takes up disk or RAM, and that waste of memory will cause more evictions or force you to upgrade your hardware.
Fixing that may save you money.
How to locate the blocks that are rarely read?
A patch to collect the data we need
Magento doesn’t provide any mechanism to spot these blocks, so I created a basic patch to store the data we need to detect them.
You have to register it in your composer.json and install magento/framework again so it is patched. More information in the README of cweagans/composer-patches.
Collecting data
Then, disable full_page, clear block_html, and browse your website. The more pages you open the more accurate your stats will be!
Exploiting the gathered data
There should now be a /var/cachestats.db file. Let open it:
$ sqlite3 cachestats.db
Now you can query the block classes that are cacheable and have a low hit rate:
sqlite> select round((sum(hits) * 1.0) / (sum(hits) + sum(misses)), 3), class, sum(hits), sum(misses) FROM block_html_cstats where cacheable = 1 group by class order by (sum(hits) * 1.0) / (sum(hits) + sum(misses));
0.0|MyVendor\CustomBlocks\Block\CustomContent\CustomDouble\Interceptor|0|1
0.0|MyVendor\CustomBlocks\Block\CustomContent\Custom\Interceptor|0|2
0.0|WeltPixel\OwlCarouselSlider\Block\Slider\Category\Interceptor|0|1
0.222|MyVendor\Header\Block\Html\TopMenu\Interceptor|4|14
0.25|MyVendor\Category\Block\Navigation\CategoryNavigation\Interceptor|2|6
0.471|MyVendor\CustomBlocks\Block\Product\Product\Interceptor|99|111
0.889|Magento\Theme\Block\Html\Footer|8|1
You could also query the block classes that are not cacheable and often generated, but if you want to make them cacheable be careful to the cache key.
sqlite> select class, sum(misses) FROM block_html_cstats where cacheable = 0 group by class order by sum(misses);
Going further
You can modify the patch to add the generation time and the size of the data, to help you determine whether a class deserve caching.