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 :)
Doing my own stuff, I am at 8 sec approximatly, but I need to get it below the 4sec load time.
I am not confortable doing further modification to the code ( I use Simple Configurable product extension to handle configurable product), and one of my attribute has thousand of entries. Would you be interested by a small contract (or a beer?) to help me implement your solution? Thanks :)
На русском варианта пока нет, но скоро у нас будет русский блог. Возможно поместим перевод этой статьи туда.
> Какое может быть минимальное время загрузки?
Универсальных решений, тем более для закустомленных магазинов нет.
Я рекомендую обратиться к нам, для консультации \ исследования http://turnkeye.com/contact_us.html
Спасибо.
This post is really interesting.
I'm experiencing the same problem as Ben Wann, the important load time is in _addProductAttributes and _addAssociatedProductFilters functions. Ben Wann, have you find a solution to increase the load speed ?
however we could not reproduce it on our database and server - the gain we saw was negligible.
if you can prove otherwise and provde us with you DB (maybe it only helps for a very specific set of products), please feel free to comment in the github ticket above.
Yes we have experience with it. I recommend to contact our team directly: http://turnkeye.com/contact_us.html
Have you any experience with optimizing that method? I have a catalog of 1200 configurable products with a total of roughly 65k simple products associated to those.
When a product list loads, ttt4 in _afterLoad is now sub 1 second (awesome) but TTT2 ( _addAssociatedProductFilters) takes roughly 40 seconds to load when a page has a decent number of results loaded per page. It looks like getUsedProducts() is the culprit in Mage_Catalog_Model_Product_Type_Configurable, but I am not all too familiar with this portion of the code base.
Line +338 of Mage_Catalog_Model_Product_Type_Configurable
->addAttributeToSelect('*')
The wildcard selection could be easily modified to only fetch _required_ attribute fields (ie. attributes used for sorting and those specified on the frontend and required for association).
Simply commenting out this line (as a test measure), doesn't radically (or noticeably in some cases) alter the catalog_product_view render, but can reduce page load times by a significant factor.