Magento performance: Optimization of Magento configurable products

If you use configurable products in your Magento store and your super attribute have a lot of options (thousands of options), you can experience the following performance issues:

  • Loading time of a configurable product page is more than other pages (especially when Magento cache is disabled)
  • When you add a configurable product to store cart, cart page become slow
  • When configurable product is added to store cart, all store pages become slow

In this article I will show how to debug such issues and how to fix the speed issue I described above.

Note: Pages load time depends on your server configuration and number of attribute options in your Magento store.

Product view loading time optimization

I started investigation of this issue on product view page. I found "TTT4" point in the Magento profiler, which take a lot of loading time (in our case 10-15 seconds).
This is a call of _loadPrices() method in the Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection class.

I have added my points to profiler and found the following code where script lose time:

foreach ($this->_items as $item) {
    $productAttribute = $item->getProductAttribute();
    if (!($productAttribute instanceof Mage_Eav_Model_Entity_Attribute_Abstract)) {
        continue;
    }
    $options = $productAttribute->getFrontend()->getSelectOptions();
    foreach ($options as $option) {
        foreach ($this->getProduct()->getTypeInstance(true)->getUsedProducts(null, $this->getProduct()) as $associatedProduct) {
             if (!empty($option['value'])
                 && $option['value'] == $associatedProduct->getData(
                                             $productAttribute->getAttributeCode())) {
                 // If option available in associated product
                 if (!isset($values[$item->getId() . ':' . $option['value']])) {
                     // If option not added, we will add it.
                     $values[$item->getId() . ':' . $option['value']] = array(
                         'product_super_attribute_id' => $item->getId(),
                         'value_index'                => $option['value'],
                         'label'                      => $option['label'],
                         'default_label'              => $option['label'],
                         'store_label'                => $option['label'],
                         'is_percent'                 => 0,
                         'pricing_value'              => null,
                         'use_default_value'          => true
                     );
                  }
              }
        }
    }
}

First of all, I noticed that this code executes 3 times for one page load and for same product ID.

Next, I created 'local cache' for this part of the code to calculate $values array one time instead of 3 times.
So I added the following property to the Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection class:

protected static $_pricings = array();

Next, I added the following code at the beginning and the end of the previous code block:

if (!Mage::app()->getStore()->isAdmin() && isset(self::$_pricings[$this->getProduct()->getId()])) {
    $values = self::$_pricings[$this->getProduct()->getId()];
} else {
    ... <- previous code block is here
    self::$_pricings[$this->getProduct()->getId()] = $values;
}

Now it around 2-3 seconds faster... not bad but the problem is still here.

Next, I added my profiler points to the 'problem' code block and found that there is two places which takes 5-7 seconds to load:

$options = $productAttribute->getFrontend()->getSelectOptions();

and

foreach ($this->getProduct()->getTypeInstance(true)->getUsedProducts(null, $this->getProduct()) as $associatedProduct) {
     if (!empty($option['value'])
         && $option['value'] == $associatedProduct->getData(
                                     $productAttribute->getAttributeCode())) {
         // If option available in associated product
         if (!isset($values[$item->getId() . ':' . $option['value']])) {
                  // If option not added, we will add it.
                  $values[$item->getId() . ':' . $option['value']] = array(
                 'product_super_attribute_id' => $item->getId(),
                 'value_index'                => $option['value'],
                 'label'                      => $option['label'],
                 'default_label'              => $option['label'],
                 'store_label'                => $option['label'],
                 'is_percent'                 => 0,
                 'pricing_value'              => null,
                 'use_default_value'          => true
               );
         }
    }
}

The second part executed thousands times (around 13000 times in my case).

This call could be moved out from this cycle, because it do not depends on any cycle variables:

$this->getProduct()->getTypeInstance(true)->getUsedProducts(null, $this->getProduct())

I have moved this call before:

foreach ($this->_items as $item) {

So, now I have the following modified code:

$__prods = $this->getProduct()->getTypeInstance(true)->getUsedProducts(null, $this->getProduct());
foreach ($this->_items as $item) {
    $productAttribute = $item->getProductAttribute();
    if (!($productAttribute instanceof Mage_Eav_Model_Entity_Attribute_Abstract)) {
        continue;
    }
    $options = $productAttribute->getFrontend()->getSelectOptions();
    foreach ($options as $option) {
        foreach ($__prods as $associatedProduct) {
             if (!empty($option['value'])
                 && $option['value'] == $associatedProduct->getData(
                                             $productAttribute->getAttributeCode())) {
                 // If option available in associated product
                 if (!isset($values[$item->getId() . ':' . $option['value']])) {
                        // If option not added, we will add it.
                        $values[$item->getId() . ':' . $option['value']] = array(
                        'product_super_attribute_id' => $item->getId(),
                        'value_index'                => $option['value'],
                        'label'                      => $option['label'],
                        'default_label'              => $option['label'],
                        'store_label'                => $option['label'],
                        'is_percent'                 => 0,
                        'pricing_value'              => null,
                        'use_default_value'          => true
                    );
                }
            }
       }
   }
}

I win 3-5 seconds. But execution of this code is still very slow:

$options = $productAttribute->getFrontend()->getSelectOptions();

Let investigate what takes so much time to load in class Mage_Eav_Model_Entity_Attribute_Frontend_Abstract:

/**
 * Get select options in case it's select box and options source is defined
 *
 * @return array
 */
public function getSelectOptions()
{
    return $this->getAttribute()->getSource()->getAllOptions();
}

Here you can see why I mention (in the beginning of this article) that this issue is connected with super attributes with many options.

As you see, the code load all super attribute options every time we load configurable product. So I created a method in the Mage_Eav_Model_Entity_Attribute_Source_Table class which allow to load options with required IDs only:

public function getNeededOptions($ids)
{
    $storeId = $this->getAttribute()->getStoreId();
        $collection = Mage::getResourceModel('eav/entity_attribute_option_collection')
            ->setPositionOrder('asc')
            ->setAttributeFilter($this->getAttribute()->getId())
            ->addFieldToFilter('main_table.option_id', array('in' => $ids))
            ->setStoreFilter($this->getAttribute()->getStoreId())
            ->load();
    return $collection->toOptionArray();
}

Next, I changed slow execution, which is:

$options = $productAttribute->getFrontend()->getSelectOptions();

To the faster one:

//  $options = $productAttribute->getFrontend()->getSelectOptions();
    $_options = array();
    foreach ($__prods as $associatedProduct) {
        $_options[] = $associatedProduct->getData($productAttribute->getAttributeCode());
    }
    $options = $productAttribute->getSource()->getNeededOptions($_options);

After these actions, product page loading time become the same as other Magento pages. The "TTT4" profiler points takes 0.1-.02 seconds.

The only issue is that the shopping cart page loading time with added configurable product is still high.

Shopping cart loading time optimization

I found the slow part of the code, it appears that it is the Mage_Catalog_Model_Product_Type_Configurable class in the getSelectedAttributesInfo() method:

$value = $value->getSource()->getOptionText($attributeValue);

Open the getOptionText() in the Mage_Eav_Model_Entity_Attribute_Source_Table class:

/**
 * Get a text for option value
 *
 * @param string|integer $value
 * @return string
 */
public function getOptionText($value)
{
    $isMultiple = false;
    if (strpos($value, ',')) {
        $isMultiple = true;
        $value = explode(',', $value);
    }

    $options = $this->getAllOptions(false);

    if ($isMultiple) {
        $values = array();
        foreach ($options as $item) {
            if (in_array($item['value'], $value)) {
                $values[] = $item['label'];
            }
        }
        return $values;
    }

    foreach ($options as $item) {
        if ($item['value'] == $value) {
            return $item['label'];
        }
    }
    return false;
}

You can see it, Magento loads all options again:

$options = $this->getAllOptions(false);

So I added my own method to the Mage_Eav_Model_Entity_Attribute_Source_Table class:

/**
 * Get a text for option value
 *
 * @param string|integer $value
 * @return string
 */
public function getNeededOptionText($value)
{
    $isMultiple = false;
    if (strpos($value, ',')) {
        $isMultiple = true;
        $value = explode(',', $value);
    }

    $options = $this->getNeededOptions($value);

    if ($isMultiple) {
        $values = array();
        foreach ($options as $item) {
            if (in_array($item['value'], $value)) {
                $values[] = $item['label'];
            }
        }
        return $values;
    }

    foreach ($options as $item) {
        if ($item['value'] == $value) {
            return $item['label'];
        }
    }
    return false;
}

And use it in the Mage_Catalog_Model_Product_Type_Configurable class at the getSelectedAttributesInfo() method:

if (!Mage::app()->getStore()->isAdmin()) {
                        $value = $value->getSource()->getNeededOptionText($attributeValue);
} else {
                        $value = $value->getSource()->getOptionText($attributeValue);
}

After all these modifications the store loading time become 10-15 seconds faster.

Conclusion

As I wrote in the beginning of this article, these performance optimization changes are very useful if you have an attribute with many options which you use for configurable products.

Also, I have noticed that I used Mage::app()->getStore()->isAdmin() condition to be on the safe side :)

Further reading:

Magento performance articles