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 :)
В результате время загрузки страницы товара выросло с 2 секунд до 13-16. После использования вашего кода скорость загрузки вернулась к прежним двум секундам. И меня радует мысль, что же будет, когда мы почистим весь мусор в БД?))
Был бы рад поработать с вами в одной команде! :)
I have a problem with the code, if I order an item from a configurable product, afther add it to cart I'm unable to add other product from the same configurable product; I dont see configurable variables if I already have the product added.
Regards.
Can you suggest a similar solution to improve stores with configurable products with many attribute (1000).
I'm working on a store that has 1000 configurable attributes for products but now all are used in the same configurable product.
will I improve speed by dividing them into many attribute sets ? rather and have all of them assigned to the default attribute set?
I'd appreciate your opinion on this matter.
I too am still having speed issues in the TTT2 section and have the SimpleConfigurableProducts module installed. Specifically I'm seeing another bucket in the profiler that says __EAV_COLLECTION_LOAD_ATTR__ that takes about 100ms per product loading into the grid. I suspect the SCP module is reloading attribute data that may already be in the result set but I can't tell yet. Any advice?