Categories
Magento

Magento Navigation Menu Caching Improvements

magento-navigation-menu

Working on different Magento based sites, I found the navigation menu being a very resource consuming part of the front-end generated pages. Most of the times, that menu was customized or even totally recreated without keeping in mind a basic thing: that menu is loaded in every single page of a Magento site and if it’s slow, every page will be slow.

The basic Magento implementation of the navigation menu is inside code/core/Mage/Catalog/Block/Navigation.php. That class uses the Magento block output caching and well written menu customization should inherit that cache.

The problem is how that cache is used: Magento caches a copy of that navigation menu per category and per user group.

You can find out that yourself looking at the getCacheKeyInfo() function, where the customer group id and the “category key” (we see more later) are used to build the cache key.

public function getCacheKeyInfo()
{
    $shortCacheId = array(
        'CATALOG_NAVIGATION',
        Mage::app()->getStore()->getId(),
        Mage::getDesign()->getPackageName(),
        Mage::getDesign()->getTheme('template'),
        Mage::getSingleton('customer/session')->getCustomerGroupId(),
        'template' => $this->getTemplate(),
        'name' => $this->getNameInLayout(),
        $this->getCurrenCategoryKey()
    );
    $cacheId = $shortCacheId;

    $shortCacheId = array_values($shortCacheId);
    $shortCacheId = implode('|', $shortCacheId);
    $shortCacheId = md5($shortCacheId);

    $cacheId['category_path'] = $this->getCurrenCategoryKey();
    $cacheId['short_cache_id'] = $shortCacheId;

    return $cacheId;
}

When you change category, the cache key changes so the navigation menu is generated for that specific category.

There are good reasons for that choice: is a user is part of a group that cannot see a category, the menu should not show it. More, the HTML generated contains classes to indicate which menu voice is currently selected, so per category caching is required.

The block add to the cache key a category key computed in this way:

public function getCurrenCategoryKey()
{
    if (!$this->_currentCategoryKey) {
        $category = Mage::registry('current_category');
        if ($category) {
            $this->_currentCategoryKey = $category->getPath();
        } else {
            $this->_currentCategoryKey = Mage::app()->getStore()->getRootCategoryId();
        }
    }

    return $this->_currentCategoryKey;
}

Hence, if you’re looking a category page (or a page where the current category is registered) the category path is used as part of the cache key, if you’re looking any other page, for example a CMS page, the root category id is used.

You can easily guess that pages not referring to a category have the same navigation menu.

Optimization and limits

If your site, as the ones I worked on, does not require a different navigation menu caching you can change the standard behavior and exclude the category from the cache key.

The best way to do that is to define a rewrite for the Navigation block creating your own class extending the original one and overriding the getCurrenCategoryKey() method, like:

public function getCurrenCategoryKey()
{
    if (!$this->_currentCategoryKey) {
        $this->_currentCategoryKey = Mage::app()->getStore()->getRootCategoryId();
    }

    return $this->_currentCategoryKey;
}

This way always the root category is used (and probably you can even get rid of it).

Now, once the navigation block is cached the first time, it is cached for every page increasing notably the performances.

About the cache key info

The getCacheKeyInfo() is a core method (defined in Mage_Core_Block_Abstract) and must return an array containing relevant parts to generate the cache key and the relative hash. This methos is used by getCacheKey() which generate the final hash.

A custom block which need to control how it’s output is cached, should override at least the getCacheKeyInfo(), to extend the array returned by the original method or to replace it totally. The original method is:

public function getCacheKeyInfo()
{
 return array(
 $this->getNameInLayout()
 );
}

Alternatively the “cache_key” data value can be used and set (for example on the block constructor) or directly override the getCacheKey() method. See the ccode below from the original Magento Mage_Core_Block_Abstract.

As a rule of thumb:

  • If you create a totally new block, you can override the getCacheKey() method: is simpler and clear
  • If you extend a core block, you should override the getCacheKeyInfo() adding to the original returned array of key parts your own, if needed
public function getCacheKey()
{
 if ($this->hasData('cache_key')) {
 return $this->getData('cache_key');
 }
 /**
 * don't prevent recalculation by saving generated cache key
 * because of ability to render single block instance with different data
 */
 $key = $this->getCacheKeyInfo();
 //ksort($key); // ignore order
 $key = array_values($key); // ignore array keys
 $key = implode('|', $key);
 $key = sha1($key);
 return $key;
}

The Magento output cache require the knowledge of two other methods and concept: the cache lifetime and the cache tags. You can find more starting from the Inchoo tutorial.

Leave a Reply